Introdução
Padrões de projeto clássicos do Gang of Four (GoF) foram concebidos para linguagens orientadas a objetos com herança. Rust não tem herança, classes ou interfaces tradicionais — em vez disso, oferece traits, enums, ownership e um sistema de tipos expressivo que permite implementar padrões de formas únicas e muitas vezes mais seguras.
Neste artigo, vamos explorar os padrões de projeto mais úteis e idiomáticos em Rust, mostrando como os recursos da linguagem transformam padrões clássicos em soluções elegantes com garantias em tempo de compilação.
O Problema: Código sem Estrutura
Não Faça Isso: Construtores com Muitos Parâmetros
// ERRADO: Construtor com muitos parâmetros — fácil errar a ordem
struct Servidor {
host: String,
porta: u16,
max_conexoes: u32,
timeout_ms: u64,
tls: bool,
certificado: Option<String>,
log_level: String,
}
impl Servidor {
fn new(
host: String,
porta: u16,
max_conexoes: u32,
timeout_ms: u64,
tls: bool,
certificado: Option<String>,
log_level: String,
) -> Self {
Servidor { host, porta, max_conexoes, timeout_ms, tls, certificado, log_level }
}
}
fn main() {
// Qual argumento é qual? Fácil trocar 100 com 5000
let server = Servidor::new(
"localhost".into(), 8080, 100, 5000, true, None, "info".into()
);
}
Não Faça Isso: Tipos Primitivos para Tudo
// ERRADO: Tipos primitivos não previnem erros lógicos
fn transferir(de: u64, para: u64, valor: f64) {
// de e para são ambos u64 — fácil trocar os argumentos
println!("Transferindo {valor} da conta {de} para conta {para}");
}
fn main() {
// Bug silencioso: argumentos trocados, compila sem problemas
transferir(999, 123, 500.0); // Queria 123 → 999 mas escreveu ao contrário
}
Padrão 1: Builder Pattern
O Builder é o padrão mais comum em Rust para construir structs complexas com validação:
/// Configuração de um servidor HTTP.
pub struct ServidorConfig {
host: String,
porta: u16,
max_conexoes: u32,
timeout_ms: u64,
tls: bool,
}
/// Builder para construir ServidorConfig passo a passo.
pub struct ServidorConfigBuilder {
host: String,
porta: u16,
max_conexoes: u32,
timeout_ms: u64,
tls: bool,
}
impl ServidorConfigBuilder {
pub fn new() -> Self {
ServidorConfigBuilder {
host: "127.0.0.1".to_string(),
porta: 8080,
max_conexoes: 100,
timeout_ms: 30_000,
tls: false,
}
}
pub fn host(mut self, host: &str) -> Self {
self.host = host.to_string();
self
}
pub fn porta(mut self, porta: u16) -> Self {
self.porta = porta;
self
}
pub fn max_conexoes(mut self, max: u32) -> Self {
self.max_conexoes = max;
self
}
pub fn timeout_ms(mut self, timeout: u64) -> Self {
self.timeout_ms = timeout;
self
}
pub fn tls(mut self, tls: bool) -> Self {
self.tls = tls;
self
}
pub fn build(self) -> Result<ServidorConfig, String> {
if self.porta == 0 {
return Err("Porta não pode ser zero".into());
}
if self.max_conexoes == 0 {
return Err("max_conexoes deve ser maior que zero".into());
}
Ok(ServidorConfig {
host: self.host,
porta: self.porta,
max_conexoes: self.max_conexoes,
timeout_ms: self.timeout_ms,
tls: self.tls,
})
}
}
fn main() {
// Legível, com valores padrão, e validação no build()
let config = ServidorConfigBuilder::new()
.host("0.0.0.0")
.porta(3000)
.max_conexoes(500)
.tls(true)
.build()
.expect("Configuração inválida");
println!("Servidor em {}:{}", config.host, config.porta);
}
Com a crate derive_builder, você pode gerar o builder automaticamente:
# Cargo.toml
[dependencies]
derive_builder = "0.20"
use derive_builder::Builder;
#[derive(Builder, Debug)]
#[builder(setter(into))]
pub struct Email {
destinatario: String,
assunto: String,
corpo: String,
#[builder(default = "false")]
html: bool,
}
fn main() {
let email = EmailBuilder::default()
.destinatario("user@example.com")
.assunto("Bem-vindo!")
.corpo("Olá, seja bem-vindo ao sistema.")
.build()
.unwrap();
println!("{:?}", email);
}
Padrão 2: Newtype Pattern
Newtype wraps um tipo primitivo em uma struct para criar um tipo distinto com semântica:
/// ID de conta bancária — tipo distinto de u64.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct ContaId(u64);
impl ContaId {
pub fn new(id: u64) -> Self {
ContaId(id)
}
pub fn valor(&self) -> u64 {
self.0
}
}
/// Valor monetário em centavos — evita erros de ponto flutuante.
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub struct Dinheiro(i64);
impl Dinheiro {
pub fn reais(valor: i64) -> Self {
Dinheiro(valor * 100)
}
pub fn centavos(valor: i64) -> Self {
Dinheiro(valor)
}
pub fn em_centavos(&self) -> i64 {
self.0
}
pub fn em_reais(&self) -> f64 {
self.0 as f64 / 100.0
}
}
impl std::fmt::Display for Dinheiro {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "R$ {:.2}", self.em_reais())
}
}
impl std::ops::Add for Dinheiro {
type Output = Self;
fn add(self, rhs: Self) -> Self {
Dinheiro(self.0 + rhs.0)
}
}
// Agora é IMPOSSÍVEL trocar conta_origem com conta_destino
fn transferir(de: ContaId, para: ContaId, valor: Dinheiro) -> Result<(), String> {
if valor.em_centavos() <= 0 {
return Err("Valor deve ser positivo".into());
}
println!("Transferindo {valor} da conta {:?} para {:?}", de, para);
Ok(())
}
fn main() {
let alice = ContaId::new(123);
let bob = ContaId::new(456);
let valor = Dinheiro::reais(500);
// transferir(bob, alice, valor) — a ordem é clara pelo nome dos tipos
transferir(alice, bob, valor).unwrap();
// Isso NÃO COMPILA — tipos diferentes
// transferir(123u64, 456u64, 500.0f64);
}
Padrão 3: Typestate Pattern
O Typestate Pattern usa o sistema de tipos para garantir que operações ocorram na ordem correta em tempo de compilação:
use std::marker::PhantomData;
// Estados (tipos sem dados, usados apenas no sistema de tipos)
pub struct Rascunho;
pub struct Revisado;
pub struct Publicado;
/// Documento cujo estado é rastreado pelo sistema de tipos.
pub struct Documento<Estado> {
titulo: String,
conteudo: String,
_estado: PhantomData<Estado>,
}
impl Documento<Rascunho> {
pub fn novo(titulo: &str) -> Self {
Documento {
titulo: titulo.to_string(),
conteudo: String::new(),
_estado: PhantomData,
}
}
pub fn escrever(&mut self, texto: &str) {
self.conteudo.push_str(texto);
}
/// Envia para revisão — consome o Rascunho e retorna Revisado
pub fn enviar_para_revisao(self) -> Documento<Revisado> {
println!("'{}' enviado para revisão", self.titulo);
Documento {
titulo: self.titulo,
conteudo: self.conteudo,
_estado: PhantomData,
}
}
}
impl Documento<Revisado> {
/// Aprova e publica — consome Revisado e retorna Publicado
pub fn aprovar(self) -> Documento<Publicado> {
println!("'{}' aprovado e publicado", self.titulo);
Documento {
titulo: self.titulo,
conteudo: self.conteudo,
_estado: PhantomData,
}
}
/// Rejeita e volta para rascunho
pub fn rejeitar(self, motivo: &str) -> Documento<Rascunho> {
println!("'{}' rejeitado: {motivo}", self.titulo);
Documento {
titulo: self.titulo,
conteudo: self.conteudo,
_estado: PhantomData,
}
}
}
impl Documento<Publicado> {
pub fn url(&self) -> String {
format!("/artigos/{}", self.titulo.to_lowercase().replace(' ', "-"))
}
}
fn main() {
// Fluxo correto: Rascunho → Revisado → Publicado
let mut doc = Documento::<Rascunho>::novo("Meu Artigo");
doc.escrever("Conteúdo do artigo...");
let doc = doc.enviar_para_revisao();
let doc = doc.aprovar();
println!("Publicado em: {}", doc.url());
// Isso NÃO COMPILA — não pode publicar diretamente do rascunho:
// let doc = Documento::<Rascunho>::novo("Teste");
// doc.aprovar(); // ERRO: método `aprovar` não existe para Documento<Rascunho>
}
Padrão 4: RAII (Resource Acquisition Is Initialization)
Rust implementa RAII nativamente com Drop. Recursos são liberados automaticamente quando saem do escopo:
use std::io::{self, Write, BufWriter};
use std::fs::File;
/// Timer que mede a duração de um escopo automaticamente.
pub struct Timer {
nome: String,
inicio: std::time::Instant,
}
impl Timer {
pub fn new(nome: &str) -> Self {
println!("[TIMER] '{}' iniciado", nome);
Timer {
nome: nome.to_string(),
inicio: std::time::Instant::now(),
}
}
}
impl Drop for Timer {
fn drop(&mut self) {
let duracao = self.inicio.elapsed();
println!("[TIMER] '{}' finalizado em {:?}", self.nome, duracao);
}
}
/// Guard para arquivo temporário que é deletado ao sair do escopo.
pub struct ArquivoTemporario {
caminho: std::path::PathBuf,
}
impl ArquivoTemporario {
pub fn new(nome: &str) -> io::Result<Self> {
let caminho = std::env::temp_dir().join(nome);
File::create(&caminho)?;
Ok(ArquivoTemporario { caminho })
}
pub fn escrever(&self, conteudo: &str) -> io::Result<()> {
let file = File::create(&self.caminho)?;
let mut writer = BufWriter::new(file);
writer.write_all(conteudo.as_bytes())?;
writer.flush()
}
pub fn caminho(&self) -> &std::path::Path {
&self.caminho
}
}
impl Drop for ArquivoTemporario {
fn drop(&mut self) {
if let Err(e) = std::fs::remove_file(&self.caminho) {
eprintln!("Aviso: não foi possível remover {:?}: {e}", self.caminho);
} else {
println!("Arquivo temporário {:?} removido", self.caminho);
}
}
}
fn main() -> io::Result<()> {
let _timer = Timer::new("main");
{
let temp = ArquivoTemporario::new("dados.tmp")?;
temp.escrever("dados temporários")?;
println!("Arquivo em: {:?}", temp.caminho());
// temp é automaticamente deletado aqui
}
println!("Arquivo temporário já foi removido");
Ok(())
}
Padrão 5: Strategy Pattern com Traits
Traits em Rust substituem interfaces e permitem polimorfismo em tempo de compilação (generics) ou em tempo de execução (trait objects):
/// Trait que define a estratégia de cálculo de desconto.
pub trait Desconto {
fn calcular(&self, valor: f64) -> f64;
fn nome(&self) -> &str;
}
pub struct SemDesconto;
impl Desconto for SemDesconto {
fn calcular(&self, valor: f64) -> f64 { valor }
fn nome(&self) -> &str { "Sem desconto" }
}
pub struct DescontoPercentual {
percentual: f64,
}
impl DescontoPercentual {
pub fn new(percentual: f64) -> Self {
DescontoPercentual { percentual }
}
}
impl Desconto for DescontoPercentual {
fn calcular(&self, valor: f64) -> f64 {
valor * (1.0 - self.percentual / 100.0)
}
fn nome(&self) -> &str { "Desconto percentual" }
}
pub struct DescontoFixo {
valor_desconto: f64,
}
impl DescontoFixo {
pub fn new(valor: f64) -> Self {
DescontoFixo { valor_desconto: valor }
}
}
impl Desconto for DescontoFixo {
fn calcular(&self, valor: f64) -> f64 {
(valor - self.valor_desconto).max(0.0)
}
fn nome(&self) -> &str { "Desconto fixo" }
}
/// Carrinho que aceita qualquer estratégia de desconto.
pub struct Carrinho {
itens: Vec<(String, f64)>,
desconto: Box<dyn Desconto>,
}
impl Carrinho {
pub fn new(desconto: Box<dyn Desconto>) -> Self {
Carrinho {
itens: Vec::new(),
desconto,
}
}
pub fn adicionar(&mut self, nome: &str, preco: f64) {
self.itens.push((nome.to_string(), preco));
}
pub fn total(&self) -> f64 {
let subtotal: f64 = self.itens.iter().map(|(_, p)| p).sum();
self.desconto.calcular(subtotal)
}
}
fn main() {
let mut carrinho = Carrinho::new(Box::new(DescontoPercentual::new(15.0)));
carrinho.adicionar("Teclado", 250.0);
carrinho.adicionar("Mouse", 150.0);
println!("Total com 15% de desconto: R$ {:.2}", carrinho.total());
let mut carrinho2 = Carrinho::new(Box::new(DescontoFixo::new(50.0)));
carrinho2.adicionar("Monitor", 1200.0);
println!("Total com R$50 de desconto: R$ {:.2}", carrinho2.total());
}
Padrão 6: Observer com Channels
Em vez de callbacks, Rust usa channels para comunicação desacoplada:
use std::sync::mpsc;
use std::thread;
#[derive(Debug, Clone)]
pub enum Evento {
UsuarioCriado { id: u64, nome: String },
PedidoRealizado { id: u64, valor: f64 },
PagamentoConfirmado { pedido_id: u64 },
}
/// Publisher que envia eventos para múltiplos subscribers.
pub struct EventBus {
senders: Vec<mpsc::Sender<Evento>>,
}
impl EventBus {
pub fn new() -> Self {
EventBus { senders: Vec::new() }
}
pub fn subscribe(&mut self) -> mpsc::Receiver<Evento> {
let (tx, rx) = mpsc::channel();
self.senders.push(tx);
rx
}
pub fn publicar(&self, evento: Evento) {
// Remove senders desconectados mantendo os ativos
for sender in &self.senders {
let _ = sender.send(evento.clone());
}
}
}
fn main() {
let mut bus = EventBus::new();
// Subscriber 1: Logger
let rx_logger = bus.subscribe();
let logger = thread::spawn(move || {
while let Ok(evento) = rx_logger.recv() {
println!("[LOG] Evento recebido: {evento:?}");
}
});
// Subscriber 2: Notificador
let rx_notif = bus.subscribe();
let notificador = thread::spawn(move || {
while let Ok(evento) = rx_notif.recv() {
if let Evento::PedidoRealizado { id, valor } = evento {
println!("[NOTIF] Novo pedido #{id}: R$ {valor:.2}");
}
}
});
// Publicar eventos
bus.publicar(Evento::UsuarioCriado {
id: 1,
nome: "Maria".into(),
});
bus.publicar(Evento::PedidoRealizado { id: 100, valor: 299.90 });
bus.publicar(Evento::PagamentoConfirmado { pedido_id: 100 });
// Fechar o bus (drop dos senders)
drop(bus);
logger.join().unwrap();
notificador.join().unwrap();
}
Padrão 7: Command Pattern com Enums
Enums em Rust são ideais para representar comandos:
/// Comandos para um editor de texto.
#[derive(Debug)]
pub enum Comando {
Inserir { posicao: usize, texto: String },
Deletar { posicao: usize, quantidade: usize },
Substituir { de: String, para: String },
}
pub struct Editor {
conteudo: String,
historico: Vec<(Comando, String)>, // (comando, estado anterior)
}
impl Editor {
pub fn new(conteudo: &str) -> Self {
Editor {
conteudo: conteudo.to_string(),
historico: Vec::new(),
}
}
pub fn executar(&mut self, comando: Comando) {
let estado_anterior = self.conteudo.clone();
match &comando {
Comando::Inserir { posicao, texto } => {
self.conteudo.insert_str(*posicao, texto);
}
Comando::Deletar { posicao, quantidade } => {
let fim = (*posicao + *quantidade).min(self.conteudo.len());
self.conteudo.drain(*posicao..fim);
}
Comando::Substituir { de, para } => {
self.conteudo = self.conteudo.replace(de, para);
}
}
self.historico.push((comando, estado_anterior));
}
pub fn desfazer(&mut self) -> bool {
if let Some((_comando, estado_anterior)) = self.historico.pop() {
self.conteudo = estado_anterior;
true
} else {
false
}
}
pub fn conteudo(&self) -> &str {
&self.conteudo
}
}
fn main() {
let mut editor = Editor::new("Olá Mundo");
println!("Inicial: '{}'", editor.conteudo());
editor.executar(Comando::Substituir {
de: "Mundo".into(),
para: "Rust".into(),
});
println!("Após substituir: '{}'", editor.conteudo());
editor.executar(Comando::Inserir {
posicao: 4,
texto: ", bem-vindo ao".into(),
});
println!("Após inserir: '{}'", editor.conteudo());
editor.desfazer();
println!("Após desfazer: '{}'", editor.conteudo());
editor.desfazer();
println!("Após desfazer: '{}'", editor.conteudo());
}
Armadilhas Comuns
1. Builder sem Validação
// ERRADO: Builder que sempre retorna Ok
impl ServidorConfigBuilder {
pub fn build(self) -> ServidorConfig {
// Nenhuma validação — aceita configuração inválida
ServidorConfig { /* ... */ }
}
}
// CORRETO: build() retorna Result
impl ServidorConfigBuilder {
pub fn build(self) -> Result<ServidorConfig, String> {
// Valida antes de construir
if self.porta == 0 {
return Err("Porta inválida".into());
}
Ok(ServidorConfig { /* ... */ })
}
}
2. Newtype sem Implementar Traits Necessários
// ERRADO: Newtype sem Debug, Clone, etc.
struct UserId(u64);
// Não pode imprimir, copiar, comparar...
// CORRETO: Derive os traits necessários
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
struct UserId(u64);
3. Trait Objects Onde Generics Bastam
// Desnecessário para um único tipo: Box<dyn> tem overhead de indireção
fn processar(estrategia: Box<dyn Desconto>, valor: f64) -> f64 {
estrategia.calcular(valor)
}
// Melhor quando o tipo é conhecido em compilação: sem overhead
fn processar<D: Desconto>(estrategia: &D, valor: f64) -> f64 {
estrategia.calcular(valor)
}
Quando Usar Cada Padrão
| Padrão | Use Quando |
|---|---|
| Builder | Struct com muitos campos ou configuração complexa |
| Newtype | Distinguir tipos primitivos com mesma representação |
| Typestate | Garantir sequência de operações em tempo de compilação |
| RAII | Gerenciar recursos (arquivos, locks, conexões) |
| Strategy | Algoritmos intercambiáveis, polimorfismo |
| Observer | Comunicação desacoplada entre componentes |
| Command | Operações reversíveis, filas de comandos |
Veja Também
- Tutorial: Traits e Generics — Fundamentos de traits para padrões de projeto
- Tutorial: Structs, Enums e Pattern Matching — Base para Newtype e Command
- Boas Práticas de Error Handling — Padrões para tipos de erro
- Documentação em Rust — Documente seus padrões com doc tests
- Migração de Python para Rust — Padrões OOP vs Rust idiomático