Introdução: Por que o SOLID é importante em Rust?
SOLID são cinco princípios fundamentais de design orientado a objetos que ajudam a criar código flexível, sustentável e escalável. Apesar de Rust não ser uma linguagem OOP clássica (não possui herança de classes da forma tradicional), esses princípios são aplicáveis aqui graças a traits, structs, genéricos e ownership de memória. Neste artigo, analisaremos cada princípio SOLID com exemplos de código em Rust, mostraremos como implementá-los e explicaremos por que eles tornam seu código melhor.
1. Single Responsibility Principle (SRP): Princípio da Responsabilidade Única
Essência: Cada módulo, struct ou função deve ter apenas um motivo para mudar.
Em Rust, isso é frequentemente violado quando uma única struct é responsável por armazenar dados, serializá-los e pela lógica de negócios. A abordagem correta é separar as responsabilidades.
Exemplo ruim (violação do SRP)
struct User { name: String, email: String,}
impl User { fn save_to_db(&self) { // lógica de salvar no banco }
fn send_email(&self, message: &str) { // lógica de enviar email }}Aqui, User é responsável tanto pelo armazenamento quanto pelo envio — dois motivos para mudar.
Exemplo bom (cumprimento do SRP)
struct User { name: String, email: String,}
struct UserRepository;impl UserRepository { fn save(&self, user: &User) { /* ... */ }}
struct EmailService;impl EmailService { fn send(&self, user: &User, message: &str) { /* ... */ }}Agora cada struct tem uma única responsabilidade: User — dados, UserRepository — salvamento, EmailService — envio.
2. Open-Closed Principle (OCP): Princípio Aberto/Fechado
Essência: Entidades de software devem estar abertas para extensão, mas fechadas para modificação.
Em Rust, isso é alcançado com traits e genéricos. Você pode adicionar novos comportamentos sem alterar o código existente.
Exemplo: Sistema de descontos
Suponha que temos uma função para calcular o preço com desconto. Em vez de adicionar novos tipos de desconto com if-else, usamos um trait:
trait Discount { fn apply(&self, price: f64) -> f64;}
struct NoDiscount;impl Discount for NoDiscount { fn apply(&self, price: f64) -> f64 { price }}
struct SeasonalDiscount(f64);impl Discount for SeasonalDiscount { fn apply(&self, price: f64) -> f64 { price * (1.0 - self.0) }}
fn calculate_price(price: f64, discount: &dyn Discount) -> f64 { discount.apply(price)}Agora, para adicionar um novo desconto (por exemplo, BlackFridayDiscount), não precisamos alterar calculate_price — basta implementar o trait Discount.
3. Liskov Substitution Principle (LSP): Princípio da Substituição de Liskov
Essência: Objetos de subtipos (em Rust, tipos que implementam um trait) devem ser intercambiáveis com objetos do tipo base sem quebrar a correção do programa.
Em Rust, isso significa que se você tem um trait e várias implementações dele, o código que trabalha com dyn Trait ou genéricos não deve quebrar ao substituir qualquer uma delas.
Exemplo de violação do LSP
trait Bird { fn fly(&self) -> String;}
struct Sparrow;impl Bird for Sparrow { fn fly(&self) -> String { "Sparrow flies".to_string() }}
struct Penguin;impl Bird for Penguin { fn fly(&self) -> String { panic!("Penguins can't fly!") }}Se substituirmos Sparrow por Penguin, o código quebra. A solução é não incluir métodos que não podem ser implementados por todos os subtipos. É melhor separar os traits:
trait CanFly { fn fly(&self) -> Stri