Design patterns são soluções recorrentes para problemas recorrentes. O livro clássico do Gang of Four catalogou 23 padrões para linguagens orientadas a objetos — mas Rust não é uma linguagem orientada a objetos tradicional. Não há herança, não há classes, não há objetos mutáveis compartilhados livremente.
Alguns padrões GOF se traduzem diretamente. Outros precisam ser reimaginados. E há padrões que emergem naturalmente do sistema de tipos de Rust — sem equivalente nos livros clássicos.
Este artigo explora os padrões mais úteis na prática, mostrando tanto como adaptá-los ao Rust idiomático quanto onde a linguagem oferece alternativas superiores.
Padrões Criacionais
Builder (revisitado)
No Artigo #35 geramos o Builder automaticamente com uma macro. Aqui exploramos a implementação manual e suas nuances:
// Builder com validação tipada em vez de Result
// Estado do builder codificado no tipo
struct SemHost;
struct ComHost(String);
struct SemPorta;
struct ComPorta(u16);
struct ServidorBuilder<H, P> {
host: H,
porta: P,
max_conexoes: usize,
timeout_ms: u64,
}
// Estado inicial — nada configurado
impl ServidorBuilder<SemHost, SemPorta> {
pub fn novo() -> Self {
ServidorBuilder {
host: SemHost,
porta: SemPorta,
max_conexoes: 100,
timeout_ms: 30_000,
}
}
}
// Só pode definir host quando ainda não tem host
impl<P> ServidorBuilder<SemHost, P> {
pub fn host(self, host: impl Into<String>) -> ServidorBuilder<ComHost, P> {
ServidorBuilder {
host: ComHost(host.into()),
porta: self.porta,
max_conexoes: self.max_conexoes,
timeout_ms: self.timeout_ms,
}
}
}
// Só pode definir porta quando ainda não tem porta
impl<H> ServidorBuilder<H, SemPorta> {
pub fn porta(self, porta: u16) -> ServidorBuilder<H, ComPorta> {
ServidorBuilder {
host: self.host,
porta: ComPorta(porta),
max_conexoes: self.max_conexoes,
timeout_ms: self.timeout_ms,
}
}
}
// Setters opcionais disponíveis em qualquer estado
impl<H, P> ServidorBuilder<H, P> {
pub fn max_conexoes(mut self, n: usize) -> Self {
self.max_conexoes = n;
self
}
pub fn timeout_ms(mut self, ms: u64) -> Self {
self.timeout_ms = ms;
self
}
}
#[derive(Debug)]
struct Servidor {
host: String,
porta: u16,
max_conexoes: usize,
timeout_ms: u64,
}
// build() só disponível quando AMBOS host e porta estão definidos
// Sem Result — o tipo garante que o build é sempre válido
impl ServidorBuilder<ComHost, ComPorta> {
pub fn build(self) -> Servidor {
Servidor {
host: self.host.0,
porta: self.porta.0,
max_conexoes: self.max_conexoes,
timeout_ms: self.timeout_ms,
}
}
}
fn main() {
let servidor = ServidorBuilder::novo()
.host("0.0.0.0")
.porta(8080)
.max_conexoes(500)
.timeout_ms(5_000)
.build(); // Sem unwrap() — nunca falha!
println!("{:?}", servidor);
// Erro em tempo de COMPILAÇÃO — não execução:
// let invalido = ServidorBuilder::novo().build();
// error: method not found: `build`
// (build só existe em ServidorBuilder<ComHost, ComPorta>)
}
Singleton
Em Rust, o Singleton é polêmico — estado global mutable viola as garantias da linguagem. A solução idiomática usa OnceLock:
use std::sync::OnceLock;
use std::collections::HashMap;
// Singleton imutável — configuração lida uma vez
static CONFIG: OnceLock<Configuracao> = OnceLock::new();
#[derive(Debug)]
struct Configuracao {
valores: HashMap<String, String>,
}
impl Configuracao {
fn carregar() -> Self {
let mut valores = HashMap::new();
// Na prática: leria de arquivo/env
valores.insert("host".to_string(), "localhost".to_string());
valores.insert("porta".to_string(), "8080".to_string());
valores.insert("env".to_string(), "producao".to_string());
Configuracao { valores }
}
pub fn global() -> &'static Configuracao {
CONFIG.get_or_init(Configuracao::carregar)
}
pub fn get(&self, chave: &str) -> Option<&str> {
self.valores.get(chave).map(String::as_str)
}
}
// Singleton mutável — use Arc<Mutex<T>>
use std::sync::{Arc, Mutex};
#[derive(Debug, Default)]
struct ContadorGlobal {
valor: u64,
}
static CONTADOR: OnceLock<Arc<Mutex<ContadorGlobal>>> = OnceLock::new();
fn contador_global() -> &'static Arc<Mutex<ContadorGlobal>> {
CONTADOR.get_or_init(|| Arc::new(Mutex::new(ContadorGlobal::default())))
}
fn main() {
// Config global — inicializada na primeira chamada
let config = Configuracao::global();
println!("Host: {:?}", config.get("host"));
println!("Env: {:?}", config.get("env"));
// A mesma instância em qualquer lugar
let config2 = Configuracao::global();
assert!(std::ptr::eq(config, config2));
// Contador global mutável
{
let mut c = contador_global().lock().unwrap();
c.valor += 1;
}
println!("Contador: {}", contador_global().lock().unwrap().valor);
}
Padrões Estruturais
Newtype — o padrão mais idiomático de Rust
Newtype não está no GOF mas é fundamental em Rust. Envolve um tipo existente para adicionar semântica:
// Sem newtype: fácil de confundir metros com quilômetros
fn velocidade(distancia: f64, tempo: f64) -> f64 {
distancia / tempo
}
// Com newtype: o compilador detecta confusões
#[derive(Debug, Clone, Copy, PartialEq, PartialOrd)]
struct Metros(f64);
#[derive(Debug, Clone, Copy, PartialEq, PartialOrd)]
struct Quilometros(f64);
#[derive(Debug, Clone, Copy)]
struct Segundos(f64);
#[derive(Debug, Clone, Copy)]
struct MetrosPorSegundo(f64);
impl Metros {
pub fn valor(self) -> f64 { self.0 }
}
impl Quilometros {
pub fn para_metros(self) -> Metros {
Metros(self.0 * 1000.0)
}
}
impl std::ops::Add for Metros {
type Output = Metros;
fn add(self, rhs: Metros) -> Metros {
Metros(self.0 + rhs.0)
}
}
fn calcular_velocidade(d: Metros, t: Segundos) -> MetrosPorSegundo {
MetrosPorSegundo(d.0 / t.0)
}
// IDs tipados — evita confundir UsuarioId com ProdutoId
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
struct UsuarioId(u64);
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
struct ProdutoId(u64);
fn buscar_usuario(id: UsuarioId) -> String {
format!("Usuario #{}", id.0)
}
fn main() {
let d = Metros(100.0);
let t = Segundos(9.58);
let v = calcular_velocidade(d, t);
println!("Velocidade: {:.2} m/s", v.0);
let km = Quilometros(1.5);
let m = km.para_metros();
println!("1.5km = {}m", m.0);
// Erro de compilação — não confunde IDs:
// buscar_usuario(ProdutoId(42)); // erro de tipo
let uid = UsuarioId(1);
println!("{}", buscar_usuario(uid));
}
Decorator / Wrapper via Traits
use std::io::{Read, Write, Result};
// Trait base — qualquer coisa que lê bytes
trait Leitor {
fn ler(&mut self, buf: &mut [u8]) -> Result<usize>;
}
// Implementação concreta
struct LeitorArquivo {
dados: Vec<u8>,
posicao: usize,
}
impl LeitorArquivo {
fn novo(dados: Vec<u8>) -> Self {
LeitorArquivo { dados, posicao: 0 }
}
}
impl Leitor for LeitorArquivo {
fn ler(&mut self, buf: &mut [u8]) -> Result<usize> {
let disponivel = &self.dados[self.posicao..];
let n = buf.len().min(disponivel.len());
buf[..n].copy_from_slice(&disponivel[..n]);
self.posicao += n;
Ok(n)
}
}
// Decorator: adiciona contagem de bytes lidos
struct LeitorComContagem<R: Leitor> {
interno: R,
bytes_lidos: usize,
}
impl<R: Leitor> LeitorComContagem<R> {
fn novo(interno: R) -> Self {
LeitorComContagem { interno, bytes_lidos: 0 }
}
fn bytes_lidos(&self) -> usize {
self.bytes_lidos
}
}
impl<R: Leitor> Leitor for LeitorComContagem<R> {
fn ler(&mut self, buf: &mut [u8]) -> Result<usize> {
let n = self.interno.ler(buf)?;
self.bytes_lidos += n;
Ok(n)
}
}
// Decorator: adiciona logging
struct LeitorComLog<R: Leitor> {
interno: R,
prefixo: String,
}
impl<R: Leitor> LeitorComLog<R> {
fn novo(interno: R, prefixo: impl Into<String>) -> Self {
LeitorComLog { interno, prefixo: prefixo.into() }
}
}
impl<R: Leitor> Leitor for LeitorComLog<R> {
fn ler(&mut self, buf: &mut [u8]) -> Result<usize> {
let n = self.interno.ler(buf)?;
println!("[{}] Lidos {} bytes", self.prefixo, n);
Ok(n)
}
}
fn main() {
let dados = b"Hello, Rust Design Patterns!".to_vec();
let arquivo = LeitorArquivo::novo(dados);
// Empilha decorators
let com_contagem = LeitorComContagem::novo(arquivo);
let mut com_log = LeitorComLog::novo(com_contagem, "LEITOR");
let mut buf = vec![0u8; 8];
com_log.ler(&mut buf).unwrap();
com_log.ler(&mut buf).unwrap();
println!("Total lido: {}", com_log.interno.bytes_lidos());
}
Facade
// Subsistemas complexos
mod audio {
pub struct SistemaAudio { inicializado: bool }
impl SistemaAudio {
pub fn novo() -> Self { SistemaAudio { inicializado: false } }
pub fn inicializar(&mut self) {
self.inicializado = true;
println!("[Audio] Inicializado");
}
pub fn reproduzir(&self, arquivo: &str) {
println!("[Audio] Reproduzindo: {arquivo}");
}
pub fn finalizar(&mut self) {
println!("[Audio] Finalizado");
}
}
}
mod video {
pub struct SistemaVideo { resolucao: (u32, u32) }
impl SistemaVideo {
pub fn novo() -> Self { SistemaVideo { resolucao: (1920, 1080) } }
pub fn inicializar(&mut self) {
println!("[Video] Inicializado {}x{}", self.resolucao.0, self.resolucao.1);
}
pub fn renderizar_frame(&self, n: u32) {
println!("[Video] Frame {n}");
}
pub fn finalizar(&mut self) {
println!("[Video] Finalizado");
}
}
}
mod rede {
pub struct SistemaRede;
impl SistemaRede {
pub fn novo() -> Self { SistemaRede }
pub fn conectar(&self, url: &str) {
println!("[Rede] Conectado a: {url}");
}
pub fn desconectar(&self) {
println!("[Rede] Desconectado");
}
}
}
// Facade: interface simples sobre os subsistemas
struct PlayerMultimidia {
audio: audio::SistemaAudio,
video: video::SistemaVideo,
rede: rede::SistemaRede,
}
impl PlayerMultimidia {
pub fn novo() -> Self {
PlayerMultimidia {
audio: audio::SistemaAudio::novo(),
video: video::SistemaVideo::novo(),
rede: rede::SistemaRede::novo(),
}
}
// Interface simples que coordena os subsistemas
pub fn reproduzir_stream(&mut self, url: &str) {
println!("── Iniciando reprodução ──");
self.rede.conectar(url);
self.audio.inicializar();
self.video.inicializar();
self.audio.reproduzir(url);
for i in 1..=3 {
self.video.renderizar_frame(i);
}
}
pub fn parar(&mut self) {
println!("── Parando reprodução ──");
self.audio.finalizar();
self.video.finalizar();
self.rede.desconectar();
}
}
fn main() {
let mut player = PlayerMultimidia::novo();
player.reproduzir_stream("https://stream.exemplo.com/video.mp4");
player.parar();
}
Padrões Comportamentais
Strategy
Em Rust, Strategy é implementado naturalmente com traits ou closures:
// Strategy com trait objects — escolha em runtime
trait EstrategiaOrdenacao {
fn ordenar(&self, dados: &mut Vec<i32>);
fn nome(&self) -> &str;
}
struct OrdenacaoBolha;
struct OrdenacaoQuick;
struct OrdenacaoMerge;
impl EstrategiaOrdenacao for OrdenacaoBolha {
fn ordenar(&self, dados: &mut Vec<i32>) {
let n = dados.len();
for i in 0..n {
for j in 0..n-i-1 {
if dados[j] > dados[j+1] {
dados.swap(j, j+1);
}
}
}
}
fn nome(&self) -> &str { "Bubble Sort" }
}
impl EstrategiaOrdenacao for OrdenacaoQuick {
fn ordenar(&self, dados: &mut Vec<i32>) {
dados.sort_unstable(); // usa introsort internamente
}
fn nome(&self) -> &str { "Quick Sort (std)" }
}
impl EstrategiaOrdenacao for OrdenacaoMerge {
fn ordenar(&self, dados: &mut Vec<i32>) {
dados.sort(); // merge sort estável
}
fn nome(&self) -> &str { "Merge Sort (std)" }
}
struct Sorter {
estrategia: Box<dyn EstrategiaOrdenacao>,
}
impl Sorter {
fn novo(estrategia: Box<dyn EstrategiaOrdenacao>) -> Self {
Sorter { estrategia }
}
fn trocar_estrategia(&mut self, estrategia: Box<dyn EstrategiaOrdenacao>) {
self.estrategia = estrategia;
}
fn ordenar(&self, dados: &mut Vec<i32>) {
use std::time::Instant;
let inicio = Instant::now();
self.estrategia.ordenar(dados);
println!("{}: {:?}", self.estrategia.nome(), inicio.elapsed());
}
}
// Strategy com closures — mais leve, sem alocação
struct ProcessadorFlexivel<F>
where
F: Fn(&str) -> String,
{
transformacao: F,
}
impl<F: Fn(&str) -> String> ProcessadorFlexivel<F> {
fn novo(transformacao: F) -> Self {
ProcessadorFlexivel { transformacao }
}
fn processar(&self, texto: &str) -> String {
(self.transformacao)(texto)
}
}
fn main() {
// Strategy com trait objects
let mut sorter = Sorter::novo(Box::new(OrdenacaoBolha));
let mut dados = vec![64, 34, 25, 12, 22, 11, 90];
sorter.ordenar(&mut dados);
println!("{:?}", dados);
sorter.trocar_estrategia(Box::new(OrdenacaoQuick));
let mut dados2 = (0..10000).rev().collect::<Vec<_>>();
sorter.ordenar(&mut dados2);
// Strategy com closures — zero overhead de alocação
let para_maiusculas = ProcessadorFlexivel::novo(|s| s.to_uppercase());
let invertido = ProcessadorFlexivel::novo(|s| {
s.chars().rev().collect()
});
let sem_espacos = ProcessadorFlexivel::novo(|s| {
s.split_whitespace().collect::<Vec<_>>().join("_")
});
for proc in [
para_maiusculas.processar("hello world"),
invertido.processar("hello world"),
sem_espacos.processar("hello world"),
] {
println!("{proc}");
}
}
Observer
use std::collections::HashMap;
type CallbackId = u64;
type Callback<T> = Box<dyn Fn(&T) + Send + Sync>;
struct EventBus<T> {
listeners: HashMap<CallbackId, Callback<T>>,
proximo_id: CallbackId,
}
impl<T> EventBus<T> {
pub fn novo() -> Self {
EventBus {
listeners: HashMap::new(),
proximo_id: 0,
}
}
pub fn assinar(&mut self, callback: impl Fn(&T) + Send + Sync + 'static) -> CallbackId {
let id = self.proximo_id;
self.proximo_id += 1;
self.listeners.insert(id, Box::new(callback));
id
}
pub fn cancelar(&mut self, id: CallbackId) {
self.listeners.remove(&id);
}
pub fn emitir(&self, evento: &T) {
for callback in self.listeners.values() {
callback(evento);
}
}
}
// Eventos tipados
#[derive(Debug, Clone)]
enum EventoEstoque {
ProdutoAdicionado { id: u32, nome: String, quantidade: u32 },
EstoqueBaixo { id: u32, nome: String, quantidade: u32, minimo: u32 },
ProdutoEsgotado { id: u32, nome: String },
}
struct SistemaEstoque {
produtos: HashMap<u32, (String, u32)>,
bus: EventBus<EventoEstoque>,
minimo_estoque: u32,
}
impl SistemaEstoque {
fn novo(minimo: u32) -> Self {
SistemaEstoque {
produtos: HashMap::new(),
bus: EventBus::novo(),
minimo_estoque: minimo,
}
}
fn assinar(&mut self, cb: impl Fn(&EventoEstoque) + Send + Sync + 'static) -> CallbackId {
self.bus.assinar(cb)
}
fn adicionar_produto(&mut self, id: u32, nome: &str, quantidade: u32) {
self.produtos.insert(id, (nome.to_string(), quantidade));
self.bus.emitir(&EventoEstoque::ProdutoAdicionado {
id,
nome: nome.to_string(),
quantidade,
});
}
fn remover_estoque(&mut self, id: u32, quantidade: u32) {
if let Some((nome, estoque)) = self.produtos.get_mut(&id) {
*estoque = estoque.saturating_sub(quantidade);
let nome_clone = nome.clone();
let estoque_atual = *estoque;
let minimo = self.minimo_estoque;
if estoque_atual == 0 {
self.bus.emitir(&EventoEstoque::ProdutoEsgotado {
id,
nome: nome_clone,
});
} else if estoque_atual < minimo {
self.bus.emitir(&EventoEstoque::EstoqueBaixo {
id,
nome: nome_clone,
quantidade: estoque_atual,
minimo,
});
}
}
}
}
fn main() {
let mut sistema = SistemaEstoque::novo(5);
// Observer: log de auditoria
sistema.assinar(|evento| {
println!("[LOG] {:?}", evento);
});
// Observer: alertas de reposição
sistema.assinar(|evento| {
match evento {
EventoEstoque::EstoqueBaixo { nome, quantidade, minimo, .. } => {
println!("⚠️ ALERTA: {} com estoque baixo ({}/{})", nome, quantidade, minimo);
}
EventoEstoque::ProdutoEsgotado { nome, .. } => {
println!("🚨 CRÍTICO: {} está ESGOTADO!", nome);
}
_ => {}
}
});
// Observer: relatório de movimentação
sistema.assinar(|evento| {
if let EventoEstoque::ProdutoAdicionado { nome, quantidade, .. } = evento {
println!("📦 Entrada: {} unidades de {}", quantidade, nome);
}
});
sistema.adicionar_produto(1, "Teclado Mecânico", 10);
sistema.remover_estoque(1, 6); // sobram 4 → EstoqueBaixo
sistema.remover_estoque(1, 4); // sobram 0 → Esgotado
}
State Machine — o padrão mais idiomático de Rust
O sistema de tipos de Rust é excepcionalmente bom para máquinas de estado:
// Máquina de estado tipada — estados são tipos diferentes
#[derive(Debug)]
struct Pedido<Estado> {
id: u64,
itens: Vec<String>,
total: f64,
estado: Estado,
}
// Estados
#[derive(Debug)] struct Rascunho;
#[derive(Debug)] struct Confirmado { confirmado_em: String }
#[derive(Debug)] struct EmTransporte { codigo_rastreio: String }
#[derive(Debug)] struct Entregue { entregue_em: String }
#[derive(Debug)] struct Cancelado { motivo: String }
// Transições válidas — definidas pelos métodos disponíveis em cada estado
impl Pedido<Rascunho> {
pub fn novo(id: u64) -> Self {
Pedido {
id,
itens: Vec::new(),
total: 0.0,
estado: Rascunho,
}
}
pub fn adicionar_item(mut self, item: &str, preco: f64) -> Self {
self.itens.push(item.to_string());
self.total += preco;
self
}
pub fn confirmar(self) -> Pedido<Confirmado> {
println!("Pedido #{} confirmado. Total: R${:.2}", self.id, self.total);
Pedido {
id: self.id,
itens: self.itens,
total: self.total,
estado: Confirmado {
confirmado_em: "2024-03-01T14:00:00Z".to_string()
},
}
}
pub fn cancelar(self, motivo: &str) -> Pedido<Cancelado> {
Pedido {
id: self.id,
itens: self.itens,
total: self.total,
estado: Cancelado { motivo: motivo.to_string() },
}
}
}
impl Pedido<Confirmado> {
pub fn enviar(self, codigo: &str) -> Pedido<EmTransporte> {
println!("Pedido #{} enviado. Rastreio: {}", self.id, codigo);
Pedido {
id: self.id,
itens: self.itens,
total: self.total,
estado: EmTransporte { codigo_rastreio: codigo.to_string() },
}
}
pub fn cancelar(self, motivo: &str) -> Pedido<Cancelado> {
println!("Pedido #{} cancelado: {}", self.id, motivo);
Pedido {
id: self.id,
itens: self.itens,
total: self.total,
estado: Cancelado { motivo: motivo.to_string() },
}
}
}
impl Pedido<EmTransporte> {
pub fn confirmar_entrega(self) -> Pedido<Entregue> {
println!("Pedido #{} entregue!", self.id);
Pedido {
id: self.id,
itens: self.itens,
total: self.total,
estado: Entregue {
entregue_em: "2024-03-03T10:30:00Z".to_string()
},
}
}
pub fn codigo_rastreio(&self) -> &str {
&self.estado.codigo_rastreio
}
}
// Pedido entregue e cancelado não têm transições — são estados finais
fn main() {
// Fluxo feliz
let entregue = Pedido::novo(1001)
.adicionar_item("Teclado", 350.0)
.adicionar_item("Mouse", 150.0)
.confirmar()
.enviar("BR123456789")
.confirmar_entrega();
println!("Entregue em: {}", entregue.estado.entregue_em);
// Cancelamento
let cancelado = Pedido::novo(1002)
.adicionar_item("Monitor", 2000.0)
.confirmar()
.cancelar("Produto indisponível");
println!("Cancelado: {}", cancelado.estado.motivo);
// Erro de COMPILAÇÃO — não execução:
// let rascunho = Pedido::novo(1003);
// rascunho.enviar("XYZ"); // enviar só existe em Confirmado!
// rascunho.confirmar_entrega(); // entrega só existe em EmTransporte!
}
Padrões idiomáticos de Rust
RAII — Resource Acquisition Is Initialization
Rust formaliza RAII através do trait Drop. Qualquer recurso que precisa ser liberado deve implementá-lo:
use std::sync::{Arc, Mutex};
struct ConexaoBD {
url: String,
id: u32,
}
impl ConexaoBD {
fn nova(url: &str, id: u32) -> Self {
println!("[BD] Conexão #{id} aberta para {url}");
ConexaoBD { url: url.to_string(), id }
}
fn executar(&self, query: &str) {
println!("[BD] #{} executando: {query}", self.id);
}
}
impl Drop for ConexaoBD {
fn drop(&mut self) {
// Garantido — executado quando o valor sai de escopo
println!("[BD] Conexão #{} fechada", self.id);
}
}
// Pool de conexões com RAII
struct GuardaConexao {
conexao: Option<ConexaoBD>,
pool: Arc<Mutex<Vec<ConexaoBD>>>,
}
impl GuardaConexao {
fn executar(&self, query: &str) {
if let Some(ref conn) = self.conexao {
conn.executar(query);
}
}
}
impl Drop for GuardaConexao {
fn drop(&mut self) {
// Devolve a conexão ao pool automaticamente
if let Some(conn) = self.conexao.take() {
println!("[Pool] Devolvendo conexão #{}", conn.id);
if let Ok(mut pool) = self.pool.lock() {
pool.push(conn);
}
}
}
}
fn main() {
{
let conn = ConexaoBD::nova("postgres://localhost/db", 1);
conn.executar("SELECT * FROM usuarios");
conn.executar("SELECT * FROM produtos");
// conn.drop() chamado aqui automaticamente
} // ← "Conexão #1 fechada" impresso aqui
println!("Após o bloco — conexão já foi fechada");
// RAII garante que conexões SEMPRE são fechadas
// mesmo em caso de panic ou retorno antecipado
}
Typestate + RAII combinados
// Arquivo que deve ser fechado explicitamente — sem esquecer
struct ArquivoAberto {
nome: String,
_privado: (), // impede construção externa
}
struct ArquivoFechado {
nome: String,
}
impl ArquivoAberto {
pub fn abrir(nome: &str) -> Self {
println!("Abrindo {nome}");
ArquivoAberto { nome: nome.to_string(), _privado: () }
}
pub fn escrever(&mut self, dados: &str) {
println!("Escrevendo em {}: {dados}", self.nome);
}
// Consome self — força fechamento explícito
pub fn fechar(self) -> ArquivoFechado {
println!("Fechando {}", self.nome);
ArquivoFechado { nome: self.nome }
}
}
// Sem impl Drop em ArquivoAberto — fechar() é obrigatório
// Se ArquivoAberto for dropped sem fechar(), é um bug que queremos detectar
impl ArquivoFechado {
pub fn nome(&self) -> &str { &self.nome }
}
fn main() {
let mut arquivo = ArquivoAberto::abrir("dados.txt");
arquivo.escrever("linha 1");
arquivo.escrever("linha 2");
let fechado = arquivo.fechar(); // obrigatório!
println!("Arquivo {} foi fechado", fechado.nome());
// Erro de compilação — não pode usar arquivo após fechar():
// arquivo.escrever("mais dados"); // moved!
}
Anti-patterns a evitar
// ❌ Anti-pattern: clone() para contornar o borrow checker
fn processar_ruim(dados: &mut Vec<String>) {
let copia = dados.clone(); // clone desnecessário
for item in &copia {
dados.push(item.to_uppercase());
}
}
// ✓ Idiomático: coleta transformações, aplica depois
fn processar_bom(dados: &mut Vec<String>) {
let novos: Vec<String> = dados.iter()
.map(|s| s.to_uppercase())
.collect();
dados.extend(novos);
}
// ❌ Anti-pattern: unwrap() em código de produção
fn buscar_usuario_ruim(id: u32) -> String {
let mapa: std::collections::HashMap<u32, &str> = [(1, "Ana")].into();
mapa.get(&id).unwrap().to_string() // pânico se não encontrar!
}
// ✓ Idiomático: propaga o erro ou fornece default
fn buscar_usuario_bom(id: u32) -> Option<String> {
let mapa: std::collections::HashMap<u32, &str> = [(1, "Ana")].into();
mapa.get(&id).map(|s| s.to_string())
}
// ❌ Anti-pattern: mutabilidade global sem controle
static mut CONTADOR: u64 = 0; // unsafe e problemático
// ✓ Idiomático: atomic ou OnceLock
use std::sync::atomic::{AtomicU64, Ordering};
static CONTADOR_ATOMICO: AtomicU64 = AtomicU64::new(0);
fn incrementar() {
CONTADOR_ATOMICO.fetch_add(1, Ordering::Relaxed);
}
Fontes e leituras recomendadas
- "Rust Design Patterns" — catálogo oficial de padrões idiomáticos — https://rust-unofficial.github.io/patterns/
- "Design Patterns: Elements of Reusable Object-Oriented Software" — GoF — o livro original, leitura obrigatória mesmo em Rust
- "Zero To Production In Rust" — Luca Palmieri — padrões aplicados em produção — https://www.zero2prod.com
- "Effective Rust" — David Drysdale — idioms específicos de Rust — https://www.lurklurk.org/effective-rust/
- "API Guidelines" — Rust team — padrões para APIs públicas — https://rust-lang.github.io/api-guidelines/
- "Typestate Pattern in Rust" — artigo aprofundado — https://cliffle.com/blog/rust-typestate/
- "Command" pattern em Rust — implementações práticas — https://refactoring.guru/design-patterns/command/rust/example