Rust

Design Patterns em Rust — Adaptando Padrões Clássicos à Linguagem Já leu

18 min de leitura

Design Patterns em Rust — Adaptando Padrões Clássicos à Linguagem
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 tradicion

 

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

Comentários

Mais em Rust

Rust e Machine Learning — Inferência, ONNX e Aceleração de Modelos
Rust e Machine Learning — Inferência, ONNX e Aceleração de Modelos

Rust — Artigo #46 Rust e Machine Learning — Inferência, ONNX e Aceleração de...

Closures Avançadas e Programação Funcional — Indo Além do map e filter
Closures Avançadas e Programação Funcional — Indo Além do map e filter

&nbsp; Em um artigo anterior, introduzimos closures e os adaptadores mais co...

Enums e Pattern Matching — O Sistema Mais Expressivo que Você Já Viu
Enums e Pattern Matching — O Sistema Mais Expressivo que Você Já Viu

&nbsp; Se structs s&atilde;o a forma de Rust agrupar dados relacionados,&nbs...