Strategy em Rust

O padrao Strategy em Rust: tres implementacoes com trait objects, closures e enums, comparacao de performance entre despacho estatico e dinamico, e exemplos praticos.

Introducao

O Strategy (Estrategia) e um padrao comportamental que permite definir uma familia de algoritmos, encapsular cada um deles e torna-los intercambiaveis. O contexto que usa o algoritmo nao precisa saber qual estrategia concreta esta sendo utilizada.

Em Rust, o Strategy e extraordinariamente versatil porque a linguagem oferece tres formas distintas de implementa-lo, cada uma com diferentes trade-offs de performance, flexibilidade e ergonomia: trait objects, closures e enums.


Problema

Voce esta construindo um sistema de processamento de dados que precisa suportar multiplos algoritmos de compressao. Diferentes clientes possuem diferentes requisitos: alguns priorizam velocidade, outros priorizam taxa de compressao, outros precisam de compatibilidade com formatos especificos.

// Sem Strategy: condicoes espalhadas por todo o codigo
fn comprimir(dados: &[u8], formato: &str) -> Vec<u8> {
    match formato {
        "gzip" => { /* logica gzip */ todo!() }
        "zstd" => { /* logica zstd */ todo!() }
        "lz4" => { /* logica lz4 */ todo!() }
        "snappy" => { /* logica snappy */ todo!() }
        // Adicionar novo formato = modificar ESTA funcao
        // E todas as outras funcoes que usam formato...
        _ => panic!("formato desconhecido"),
    }
}

Solucao em Rust

Abordagem 1: Trait Objects (Despacho Dinamico)

A forma mais flexivel, ideal quando as estrategias sao definidas em tempo de execucao:

/// Trait que define a interface da estrategia
pub trait Compressor: Send + Sync {
    /// Comprime os dados de entrada
    fn comprimir(&self, dados: &[u8]) -> Vec<u8>;

    /// Descomprime os dados
    fn descomprimir(&self, dados: &[u8]) -> Vec<u8>;

    /// Retorna o nome do algoritmo
    fn nome(&self) -> &str;

    /// Estima a taxa de compressao (menor = melhor compressao)
    fn taxa_estimada(&self) -> f64;
}

/// Estrategia: compressao simulada por run-length encoding
pub struct RleCompressor;

impl Compressor for RleCompressor {
    fn comprimir(&self, dados: &[u8]) -> Vec<u8> {
        let mut resultado = Vec::new();
        let mut i = 0;

        while i < dados.len() {
            let byte_atual = dados[i];
            let mut contagem: u8 = 1;

            while i + contagem as usize < dados.len()
                && dados[i + contagem as usize] == byte_atual
                && contagem < 255
            {
                contagem += 1;
            }

            resultado.push(contagem);
            resultado.push(byte_atual);
            i += contagem as usize;
        }

        resultado
    }

    fn descomprimir(&self, dados: &[u8]) -> Vec<u8> {
        let mut resultado = Vec::new();

        for chunk in dados.chunks(2) {
            if chunk.len() == 2 {
                for _ in 0..chunk[0] {
                    resultado.push(chunk[1]);
                }
            }
        }

        resultado
    }

    fn nome(&self) -> &str {
        "RLE (Run-Length Encoding)"
    }

    fn taxa_estimada(&self) -> f64 {
        0.7 // eficiente para dados repetitivos
    }
}

/// Estrategia: compressao por dicionario simples
pub struct DicionarioCompressor {
    tamanho_janela: usize,
}

impl DicionarioCompressor {
    pub fn new(tamanho_janela: usize) -> Self {
        Self { tamanho_janela }
    }
}

impl Compressor for DicionarioCompressor {
    fn comprimir(&self, dados: &[u8]) -> Vec<u8> {
        // Implementacao simplificada para demonstracao
        println!(
            "[Dicionario] Comprimindo {} bytes com janela de {}",
            dados.len(),
            self.tamanho_janela
        );
        // Simulacao: em producao usaria LZ77/LZ78
        let mut resultado = dados.to_vec();
        resultado.push(0xFF); // marcador
        resultado
    }

    fn descomprimir(&self, dados: &[u8]) -> Vec<u8> {
        // Remove o marcador
        let mut resultado = dados.to_vec();
        if resultado.last() == Some(&0xFF) {
            resultado.pop();
        }
        resultado
    }

    fn nome(&self) -> &str {
        "Dicionario (LZ-like)"
    }

    fn taxa_estimada(&self) -> f64 {
        0.4 // boa taxa de compressao
    }
}

/// Estrategia: sem compressao (passthrough)
pub struct SemCompressao;

impl Compressor for SemCompressao {
    fn comprimir(&self, dados: &[u8]) -> Vec<u8> {
        dados.to_vec()
    }

    fn descomprimir(&self, dados: &[u8]) -> Vec<u8> {
        dados.to_vec()
    }

    fn nome(&self) -> &str {
        "Nenhuma (passthrough)"
    }

    fn taxa_estimada(&self) -> f64 {
        1.0 // sem compressao
    }
}

/// Contexto que usa a estrategia de compressao
pub struct ProcessadorArquivos {
    compressor: Box<dyn Compressor>,
    nome: String,
}

impl ProcessadorArquivos {
    pub fn new(nome: &str, compressor: Box<dyn Compressor>) -> Self {
        Self {
            compressor,
            nome: nome.to_string(),
        }
    }

    /// Troca a estrategia em tempo de execucao
    pub fn trocar_compressor(&mut self, compressor: Box<dyn Compressor>) {
        println!(
            "[{}] Trocando compressor: {} -> {}",
            self.nome,
            self.compressor.nome(),
            compressor.nome()
        );
        self.compressor = compressor;
    }

    /// Processa dados usando a estrategia atual
    pub fn processar(&self, dados: &[u8]) -> Vec<u8> {
        println!(
            "[{}] Processando {} bytes com '{}'",
            self.nome,
            dados.len(),
            self.compressor.nome()
        );

        let comprimido = self.compressor.comprimir(dados);

        let taxa = comprimido.len() as f64 / dados.len() as f64;
        println!(
            "[{}] Resultado: {} -> {} bytes (taxa: {:.1}%)",
            self.nome,
            dados.len(),
            comprimido.len(),
            taxa * 100.0
        );

        comprimido
    }
}

fn main() {
    // Selecao de estrategia em tempo de execucao
    let dados = b"AAAAAABBBBCCCCCCCCDDDDDDDDDDDD";

    let mut proc = ProcessadorArquivos::new("Servidor", Box::new(RleCompressor));
    let comprimido = proc.processar(dados);

    // Verifica integridade
    let descomprimido = RleCompressor.descomprimir(&comprimido);
    assert_eq!(dados.as_slice(), descomprimido.as_slice());
    println!("Integridade verificada!\n");

    // Troca estrategia em tempo de execucao
    proc.trocar_compressor(Box::new(DicionarioCompressor::new(4096)));
    proc.processar(dados);
}

Abordagem 2: Closures (Funcional)

Ideal para estrategias simples e composicao funcional:

/// Tipo para funcao de ordenacao customizada
type ComparadorFn<T> = Box<dyn Fn(&T, &T) -> std::cmp::Ordering>;

#[derive(Debug, Clone)]
pub struct Produto {
    pub nome: String,
    pub preco: f64,
    pub avaliacao: f64,
    pub vendas: u64,
}

/// Fabrica de estrategias de ordenacao usando closures
pub mod estrategias_ordenacao {
    use super::*;

    pub fn por_preco_crescente() -> ComparadorFn<Produto> {
        Box::new(|a, b| {
            a.preco
                .partial_cmp(&b.preco)
                .unwrap_or(std::cmp::Ordering::Equal)
        })
    }

    pub fn por_preco_decrescente() -> ComparadorFn<Produto> {
        Box::new(|a, b| {
            b.preco
                .partial_cmp(&a.preco)
                .unwrap_or(std::cmp::Ordering::Equal)
        })
    }

    pub fn por_avaliacao() -> ComparadorFn<Produto> {
        Box::new(|a, b| {
            b.avaliacao
                .partial_cmp(&a.avaliacao)
                .unwrap_or(std::cmp::Ordering::Equal)
        })
    }

    pub fn por_popularidade() -> ComparadorFn<Produto> {
        Box::new(|a, b| b.vendas.cmp(&a.vendas))
    }

    /// Estrategia composta: ordena por multiplos criterios
    pub fn multi_criterio(
        criterios: Vec<ComparadorFn<Produto>>,
    ) -> ComparadorFn<Produto> {
        Box::new(move |a, b| {
            for criterio in &criterios {
                let resultado = criterio(a, b);
                if resultado != std::cmp::Ordering::Equal {
                    return resultado;
                }
            }
            std::cmp::Ordering::Equal
        })
    }
}

fn demonstrar_closures() {
    let mut produtos = vec![
        Produto { nome: "Notebook".into(), preco: 4500.0, avaliacao: 4.5, vendas: 120 },
        Produto { nome: "Mouse".into(), preco: 89.90, avaliacao: 4.8, vendas: 5000 },
        Produto { nome: "Teclado".into(), preco: 250.0, avaliacao: 4.2, vendas: 800 },
        Produto { nome: "Monitor".into(), preco: 1800.0, avaliacao: 4.6, vendas: 300 },
        Produto { nome: "Fone".into(), preco: 350.0, avaliacao: 4.8, vendas: 2000 },
    ];

    // Estrategia 1: por preco
    let comparador = estrategias_ordenacao::por_preco_crescente();
    produtos.sort_by(|a, b| comparador(a, b));
    println!("=== Por preco (crescente) ===");
    for p in &produtos {
        println!("  {} - R${:.2}", p.nome, p.preco);
    }

    // Estrategia 2: por avaliacao
    let comparador = estrategias_ordenacao::por_avaliacao();
    produtos.sort_by(|a, b| comparador(a, b));
    println!("\n=== Por avaliacao (melhor primeiro) ===");
    for p in &produtos {
        println!("  {} - {:.1} estrelas", p.nome, p.avaliacao);
    }

    // Estrategia 3: multi-criterio (avaliacao, depois vendas)
    let comparador = estrategias_ordenacao::multi_criterio(vec![
        estrategias_ordenacao::por_avaliacao(),
        estrategias_ordenacao::por_popularidade(),
    ]);
    produtos.sort_by(|a, b| comparador(a, b));
    println!("\n=== Por avaliacao + popularidade ===");
    for p in &produtos {
        println!(
            "  {} - {:.1} estrelas, {} vendas",
            p.nome, p.avaliacao, p.vendas
        );
    }
}

Abordagem 3: Enums (Despacho Estatico)

Maximo desempenho quando as estrategias sao conhecidas em tempo de compilacao:

/// Estrategia de calculo de frete como enum
#[derive(Debug, Clone)]
pub enum EstrategiaFrete {
    /// Frete fixo por regiao
    Fixo { valor: f64 },

    /// Frete calculado por peso
    PorPeso { preco_por_kg: f64, minimo: f64 },

    /// Frete gratis acima de um valor
    GratisAcimaDe { limite: f64, frete_padrao: f64 },

    /// Frete expresso (percentual do pedido)
    Expresso { percentual: f64, minimo: f64 },
}

impl EstrategiaFrete {
    /// Calcula o valor do frete - despacho estatico via match
    pub fn calcular(&self, valor_pedido: f64, peso_kg: f64) -> f64 {
        match self {
            Self::Fixo { valor } => *valor,

            Self::PorPeso { preco_por_kg, minimo } => {
                let calculado = peso_kg * preco_por_kg;
                calculado.max(*minimo)
            }

            Self::GratisAcimaDe { limite, frete_padrao } => {
                if valor_pedido >= *limite {
                    0.0
                } else {
                    *frete_padrao
                }
            }

            Self::Expresso { percentual, minimo } => {
                let calculado = valor_pedido * percentual / 100.0;
                calculado.max(*minimo)
            }
        }
    }

    pub fn descricao(&self) -> String {
        match self {
            Self::Fixo { valor } => format!("Fixo: R${:.2}", valor),
            Self::PorPeso { preco_por_kg, .. } => {
                format!("Por peso: R${:.2}/kg", preco_por_kg)
            }
            Self::GratisAcimaDe { limite, .. } => {
                format!("Gratis acima de R${:.2}", limite)
            }
            Self::Expresso { percentual, .. } => {
                format!("Expresso: {}% do pedido", percentual)
            }
        }
    }
}

fn demonstrar_enums() {
    let estrategias = vec![
        EstrategiaFrete::Fixo { valor: 15.90 },
        EstrategiaFrete::PorPeso { preco_por_kg: 5.0, minimo: 10.0 },
        EstrategiaFrete::GratisAcimaDe { limite: 200.0, frete_padrao: 25.0 },
        EstrategiaFrete::Expresso { percentual: 5.0, minimo: 20.0 },
    ];

    let valor_pedido = 350.0;
    let peso = 2.5;

    println!("Pedido: R${:.2}, Peso: {:.1}kg\n", valor_pedido, peso);

    for estrategia in &estrategias {
        let frete = estrategia.calcular(valor_pedido, peso);
        println!(
            "  {}: R${:.2}",
            estrategia.descricao(),
            frete
        );
    }
}

Diagrama

STRATEGY COM TRAIT OBJECTS:

    +---------------------+         +------------------+
    | ProcessadorArquivos |-------->| dyn Compressor   |
    |                     |         +------------------+
    | compressor: Box<    |              ^    ^    ^
    |   dyn Compressor>   |              |    |    |
    +---------------------+              |    |    |
                                         |    |    |
              +--------------------------+    |    +------------------+
              |                               |                      |
    +---------+--------+    +---------+--------+    +--------+-------+
    | RleCompressor    |    | DicionarioCompr  |    | SemCompressao  |
    +------------------+    +------------------+    +----------------+


STRATEGY COM CLOSURES:

    sort_by(|a, b| comparador(a, b))
                      |
                      v
            +------------------+
            | Box<dyn Fn(      |
            |   &T, &T         |
            | ) -> Ordering>   |
            +------------------+
            Qualquer closure serve!


STRATEGY COM ENUM:

    match self {
        Fixo { .. } => ...,        <-- despacho em tempo
        PorPeso { .. } => ...,         de compilacao
        Expresso { .. } => ...,        (sem vtable)
    }

Exemplo do Mundo Real

Sistema de validacao com estrategias combinaveis:

/// Resultado de uma validacao
#[derive(Debug)]
pub struct ErroValidacao {
    pub campo: String,
    pub mensagem: String,
}

/// Estrategia de validacao para campos de formulario
pub trait Validador: Send + Sync {
    fn validar(&self, campo: &str, valor: &str) -> Result<(), ErroValidacao>;
    fn descricao(&self) -> &str;
}

pub struct NaoVazio;

impl Validador for NaoVazio {
    fn validar(&self, campo: &str, valor: &str) -> Result<(), ErroValidacao> {
        if valor.trim().is_empty() {
            Err(ErroValidacao {
                campo: campo.to_string(),
                mensagem: "Campo nao pode ser vazio".to_string(),
            })
        } else {
            Ok(())
        }
    }
    fn descricao(&self) -> &str { "nao vazio" }
}

pub struct TamanhoMinimo(pub usize);

impl Validador for TamanhoMinimo {
    fn validar(&self, campo: &str, valor: &str) -> Result<(), ErroValidacao> {
        if valor.len() < self.0 {
            Err(ErroValidacao {
                campo: campo.to_string(),
                mensagem: format!("Minimo de {} caracteres (tem {})", self.0, valor.len()),
            })
        } else {
            Ok(())
        }
    }
    fn descricao(&self) -> &str { "tamanho minimo" }
}

pub struct EmailValido;

impl Validador for EmailValido {
    fn validar(&self, campo: &str, valor: &str) -> Result<(), ErroValidacao> {
        if !valor.contains('@') || !valor.contains('.') {
            Err(ErroValidacao {
                campo: campo.to_string(),
                mensagem: "Email invalido".to_string(),
            })
        } else {
            Ok(())
        }
    }
    fn descricao(&self) -> &str { "email valido" }
}

/// Combinador de validadores (Strategy + Composite)
pub struct ValidadorComposto {
    validadores: Vec<Box<dyn Validador>>,
}

impl ValidadorComposto {
    pub fn new() -> Self {
        Self { validadores: Vec::new() }
    }

    pub fn adicionar(mut self, v: Box<dyn Validador>) -> Self {
        self.validadores.push(v);
        self
    }

    pub fn validar_campo(&self, campo: &str, valor: &str) -> Vec<ErroValidacao> {
        self.validadores
            .iter()
            .filter_map(|v| v.validar(campo, valor).err())
            .collect()
    }
}

fn main() {
    // Compoe estrategias de validacao
    let validador_email = ValidadorComposto::new()
        .adicionar(Box::new(NaoVazio))
        .adicionar(Box::new(TamanhoMinimo(5)))
        .adicionar(Box::new(EmailValido));

    let validador_senha = ValidadorComposto::new()
        .adicionar(Box::new(NaoVazio))
        .adicionar(Box::new(TamanhoMinimo(8)));

    // Testa com valores diferentes
    let testes = vec![
        ("email", "", &validador_email),
        ("email", "ab", &validador_email),
        ("email", "usuario@email.com", &validador_email),
        ("senha", "123", &validador_senha),
        ("senha", "senhaforte123", &validador_senha),
    ];

    for (campo, valor, validador) in testes {
        let erros = validador.validar_campo(campo, valor);
        if erros.is_empty() {
            println!("{} = '{}' -> OK", campo, valor);
        } else {
            println!("{} = '{}' -> ERROS:", campo, valor);
            for e in erros {
                println!("    - {}", e.mensagem);
            }
        }
    }
}

Quando Usar

  • Multiplos algoritmos intercambiaveis para a mesma tarefa
  • Selecao de algoritmo em tempo de execucao (configuracao, input do usuario)
  • Eliminar condicionais extensos (substituir match/if-else por polimorfismo)
  • Testes - facilita trocar implementacao real por mock
  • Combinacao de comportamentos (validacao, filtros, pipelines)

Quando NAO Usar

  • Apenas 2-3 variantes simples - um match direto e mais claro
  • O algoritmo nunca muda em tempo de execucao - use genericos com traits
  • Overhead de trait objects importa - prefira enums ou genericos

Variacoes em Rust

1. Strategy com genericos (custo zero)

// Despacho estatico - sem vtable, inlining possivel
pub struct Processador<C: Compressor> {
    compressor: C,
}

impl<C: Compressor> Processador<C> {
    pub fn processar(&self, dados: &[u8]) -> Vec<u8> {
        self.compressor.comprimir(dados)
    }
}
// O tipo concreto e fixo em tempo de compilacao

2. Strategy com fn pointer

// Para estrategias sem estado, fn pointers sao mais leves que Box<dyn Fn>
type Formatador = fn(f64) -> String;

fn real_brasileiro(valor: f64) -> String {
    format!("R${:.2}", valor)
}

fn dolar(valor: f64) -> String {
    format!("${:.2}", valor)
}

struct Relatorio {
    formatador: Formatador,
}

3. Strategy com enum + dados

// Combina a performance do enum com flexibilidade de dados associados
enum Desconto {
    Percentual(f64),
    Fixo(f64),
    CompraMinima { minimo: f64, desconto_pct: f64 },
}

impl Desconto {
    fn aplicar(&self, valor: f64) -> f64 {
        match self {
            Self::Percentual(pct) => valor * (1.0 - pct / 100.0),
            Self::Fixo(desc) => (valor - desc).max(0.0),
            Self::CompraMinima { minimo, desconto_pct } => {
                if valor >= *minimo {
                    valor * (1.0 - desconto_pct / 100.0)
                } else {
                    valor
                }
            }
        }
    }
}

Padroes Relacionados

  • Factory - Factory pode criar a estrategia adequada
  • Decorator - Decorator empilha comportamento; Strategy troca comportamento
  • Observer - Observer notifica sobre mudancas; Strategy muda o como algo e feito
  • Composite - Strategies podem ser compostas em arvore (validadores compostos)

Conclusao

O Strategy em Rust oferece uma riqueza unica de implementacoes. Trait objects fornecem flexibilidade maxima com despacho dinamico. Closures permitem estrategias ad-hoc sem definir novos tipos. Enums oferecem performance maxima com despacho estatico. E genericos com trait bounds dao custo zero com monomorfizacao. A escolha entre essas abordagens depende do cenario: se a estrategia e conhecida em tempo de compilacao, prefira genericos ou enums; se precisa trocar em tempo de execucao, use trait objects. Essa versatilidade torna o Strategy um dos padroes mais poderosos e idiomaticos em Rust.