Factory Method em Rust

Aprenda o padrao Factory Method em Rust: criacao de objetos via trait objects, enums como fabricas, Box<dyn Trait> para polimorfismo em tempo de execucao e exemplos praticos.

Introducao

O Factory Method (Metodo Fabrica) e um padrao criacional que define uma interface para criar objetos, mas permite que as subclasses (ou, em Rust, os implementadores de traits) decidam qual tipo concreto instanciar. Em vez de chamar new() diretamente em um tipo concreto, voce delega a criacao para uma funcao ou trait que decide qual tipo criar com base em parametros ou configuracao.

Em Rust, o Factory Method se manifesta de formas diferentes das linguagens orientadas a objetos tradicionais. Sem heranca de classes, usamos traits, enums e funcoes livres para obter o mesmo resultado, muitas vezes com mais seguranca e performance.


Problema

Voce esta construindo um sistema que precisa processar arquivos de configuracao em diversos formatos: JSON, YAML e TOML. Cada formato tem sua propria logica de parsing, mas o resto do sistema nao deve se preocupar com qual formato esta sendo usado.

Sem o Factory, seu codigo ficaria cheio de condicionais:

// Codigo fragil - cada novo formato exige modificacao aqui
fn carregar_config(caminho: &str) -> Result<Config, Erro> {
    if caminho.ends_with(".json") {
        // logica para JSON...
        let conteudo = std::fs::read_to_string(caminho)?;
        // parse JSON manualmente
        todo!()
    } else if caminho.ends_with(".yaml") || caminho.ends_with(".yml") {
        // logica para YAML...
        todo!()
    } else if caminho.ends_with(".toml") {
        // logica para TOML...
        todo!()
    } else {
        // E se quisermos adicionar XML? INI? HCL?
        // Precisamos mexer nessa funcao toda vez!
        Err(Erro::FormatoDesconhecido)
    }
}

Esse codigo viola o principio Aberto/Fechado: cada novo formato requer modificacao no codigo existente, em vez de simplesmente estender o sistema.


Solucao em Rust

Abordagem 1: Factory com Trait Objects

A abordagem mais flexivel usa Box<dyn Trait> para retornar diferentes implementacoes:

use std::collections::HashMap;
use std::path::Path;

/// Erro unificado para operacoes de parsing
#[derive(Debug)]
pub enum ParserError {
    FormatoDesconhecido(String),
    ErroSintaxe { linha: usize, mensagem: String },
    IoError(std::io::Error),
}

/// Valor generico representando dados de configuracao
#[derive(Debug, Clone)]
pub enum Valor {
    Texto(String),
    Inteiro(i64),
    Decimal(f64),
    Booleano(bool),
    Lista(Vec<Valor>),
    Mapa(HashMap<String, Valor>),
    Nulo,
}

/// Trait que define a interface comum para todos os parsers
pub trait Parser: Send + Sync {
    /// Faz o parse de uma string e retorna a estrutura de dados
    fn parse(&self, conteudo: &str) -> Result<Valor, ParserError>;

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

    /// Retorna as extensoes de arquivo suportadas
    fn extensoes(&self) -> &[&str];
}

/// Parser para formato JSON
pub struct JsonParser;

impl Parser for JsonParser {
    fn parse(&self, conteudo: &str) -> Result<Valor, ParserError> {
        // Implementacao simplificada para demonstracao
        // Em producao, usaria serde_json
        println!("[JSON] Fazendo parse de {} bytes", conteudo.len());

        // Simulacao de parse basico
        if conteudo.trim().starts_with('{') {
            Ok(Valor::Mapa(HashMap::new()))
        } else {
            Err(ParserError::ErroSintaxe {
                linha: 1,
                mensagem: "JSON deve comecar com '{'".to_string(),
            })
        }
    }

    fn formato(&self) -> &str {
        "JSON"
    }

    fn extensoes(&self) -> &[&str] {
        &["json"]
    }
}

/// Parser para formato TOML
pub struct TomlParser;

impl Parser for TomlParser {
    fn parse(&self, conteudo: &str) -> Result<Valor, ParserError> {
        println!("[TOML] Fazendo parse de {} bytes", conteudo.len());

        // Simulacao: procura por pares chave=valor
        let mut mapa = HashMap::new();
        for linha in conteudo.lines() {
            if let Some((chave, valor)) = linha.split_once('=') {
                mapa.insert(
                    chave.trim().to_string(),
                    Valor::Texto(valor.trim().trim_matches('"').to_string()),
                );
            }
        }
        Ok(Valor::Mapa(mapa))
    }

    fn formato(&self) -> &str {
        "TOML"
    }

    fn extensoes(&self) -> &[&str] {
        &["toml"]
    }
}

/// Parser para formato YAML
pub struct YamlParser;

impl Parser for YamlParser {
    fn parse(&self, conteudo: &str) -> Result<Valor, ParserError> {
        println!("[YAML] Fazendo parse de {} bytes", conteudo.len());
        Ok(Valor::Mapa(HashMap::new()))
    }

    fn formato(&self) -> &str {
        "YAML"
    }

    fn extensoes(&self) -> &[&str] {
        &["yaml", "yml"]
    }
}

/// Fabrica que cria o parser adequado com base na extensao do arquivo
pub struct ParserFactory {
    parsers: Vec<Box<dyn Parser>>,
}

impl ParserFactory {
    pub fn new() -> Self {
        Self {
            parsers: Vec::new(),
        }
    }

    /// Registra um novo parser na fabrica
    pub fn registrar(mut self, parser: Box<dyn Parser>) -> Self {
        self.parsers.push(parser);
        self
    }

    /// Cria o parser adequado para o caminho do arquivo
    pub fn criar_para_arquivo(&self, caminho: &str) -> Result<&dyn Parser, ParserError> {
        let extensao = Path::new(caminho)
            .extension()
            .and_then(|ext| ext.to_str())
            .unwrap_or("");

        self.parsers
            .iter()
            .find(|p| p.extensoes().contains(&extensao))
            .map(|p| p.as_ref())
            .ok_or_else(|| ParserError::FormatoDesconhecido(extensao.to_string()))
    }

    /// Lista todos os formatos suportados
    pub fn formatos_suportados(&self) -> Vec<&str> {
        self.parsers.iter().map(|p| p.formato()).collect()
    }
}

fn main() {
    // Configura a fabrica com todos os parsers disponiveis
    let fabrica = ParserFactory::new()
        .registrar(Box::new(JsonParser))
        .registrar(Box::new(TomlParser))
        .registrar(Box::new(YamlParser));

    println!(
        "Formatos suportados: {:?}",
        fabrica.formatos_suportados()
    );

    // A fabrica escolhe o parser correto automaticamente
    let arquivos = vec![
        "config.json",
        "Cargo.toml",
        "docker-compose.yaml",
        "dados.yml",
    ];

    for arquivo in arquivos {
        match fabrica.criar_para_arquivo(arquivo) {
            Ok(parser) => {
                println!("\n{}: usando parser {}", arquivo, parser.formato());
                let _ = parser.parse("{ \"exemplo\": true }");
            }
            Err(e) => println!("\n{}: erro - {:?}", arquivo, e),
        }
    }
}

Abordagem 2: Factory com Enum (Despacho Estatico)

Quando os tipos possiveis sao conhecidos em tempo de compilacao, enums evitam alocacoes:

/// Enum que representa todos os parsers possiveis
#[derive(Debug, Clone)]
pub enum ParserEnum {
    Json,
    Toml,
    Yaml,
}

impl ParserEnum {
    /// Factory method: cria o parser baseado na extensao
    pub fn para_extensao(ext: &str) -> Result<Self, String> {
        match ext {
            "json" => Ok(Self::Json),
            "toml" => Ok(Self::Toml),
            "yaml" | "yml" => Ok(Self::Yaml),
            outro => Err(format!("Extensao '{}' nao suportada", outro)),
        }
    }

    /// Factory method: cria baseado no conteudo (detecta formato)
    pub fn detectar_formato(conteudo: &str) -> Self {
        let trimmed = conteudo.trim();
        if trimmed.starts_with('{') || trimmed.starts_with('[') {
            Self::Json
        } else if trimmed.contains("---") {
            Self::Yaml
        } else {
            Self::Toml // padrao
        }
    }

    /// Executa o parse usando o parser apropriado
    pub fn parse(&self, conteudo: &str) -> Result<String, String> {
        match self {
            Self::Json => {
                println!("Parseando como JSON...");
                Ok(format!("JSON parseado: {} bytes", conteudo.len()))
            }
            Self::Toml => {
                println!("Parseando como TOML...");
                Ok(format!("TOML parseado: {} bytes", conteudo.len()))
            }
            Self::Yaml => {
                println!("Parseando como YAML...");
                Ok(format!("YAML parseado: {} bytes", conteudo.len()))
            }
        }
    }
}

fn main() {
    // Factory via extensao
    let parser = ParserEnum::para_extensao("json").unwrap();
    let resultado = parser.parse(r#"{"chave": "valor"}"#).unwrap();
    println!("{}", resultado);

    // Factory via deteccao automatica
    let parser = ParserEnum::detectar_formato("[1, 2, 3]");
    let resultado = parser.parse("[1, 2, 3]").unwrap();
    println!("{}", resultado);
}

Diagrama

FACTORY COM TRAIT OBJECTS:

    +------------------+
    |  ParserFactory   |
    |  (fabrica)       |
    +--------+---------+
             |
             | criar_para_arquivo("config.json")
             |
             v
    +------------------+        +------------------+
    | dyn Parser       |<-------| JsonParser       |
    | (trait object)   |        +------------------+
    +------------------+        | TomlParser       |
                                +------------------+
                                | YamlParser       |
                                +------------------+

FACTORY COM ENUM:

    ParserEnum::para_extensao("toml")
             |
             v
    +------------------+
    | ParserEnum::Toml |----> parse() via match
    +------------------+

    Sem alocacao no heap!
    Despacho estatico, nao virtual.

Exemplo do Mundo Real

Uma fabrica de conexoes de banco de dados que suporta multiplos backends:

use std::collections::HashMap;

/// Resultado de uma consulta ao banco de dados
#[derive(Debug)]
pub struct ResultadoConsulta {
    pub colunas: Vec<String>,
    pub linhas: Vec<Vec<String>>,
}

/// Trait para conexoes de banco de dados
pub trait ConexaoBanco: Send + Sync {
    fn conectar(&mut self) -> Result<(), String>;
    fn executar(&self, sql: &str) -> Result<ResultadoConsulta, String>;
    fn desconectar(&mut self) -> Result<(), String>;
    fn nome_driver(&self) -> &str;
}

/// Conexao PostgreSQL
pub struct ConexaoPostgres {
    url: String,
    conectado: bool,
}

impl ConexaoBanco for ConexaoPostgres {
    fn conectar(&mut self) -> Result<(), String> {
        println!("[PostgreSQL] Conectando a {}...", self.url);
        self.conectado = true;
        Ok(())
    }

    fn executar(&self, sql: &str) -> Result<ResultadoConsulta, String> {
        if !self.conectado {
            return Err("Nao conectado ao PostgreSQL".to_string());
        }
        println!("[PostgreSQL] Executando: {}", sql);
        Ok(ResultadoConsulta {
            colunas: vec!["id".into(), "nome".into()],
            linhas: vec![vec!["1".into(), "Maria".into()]],
        })
    }

    fn desconectar(&mut self) -> Result<(), String> {
        println!("[PostgreSQL] Desconectando...");
        self.conectado = false;
        Ok(())
    }

    fn nome_driver(&self) -> &str {
        "PostgreSQL"
    }
}

/// Conexao SQLite
pub struct ConexaoSqlite {
    caminho: String,
    conectado: bool,
}

impl ConexaoBanco for ConexaoSqlite {
    fn conectar(&mut self) -> Result<(), String> {
        println!("[SQLite] Abrindo banco em {}...", self.caminho);
        self.conectado = true;
        Ok(())
    }

    fn executar(&self, sql: &str) -> Result<ResultadoConsulta, String> {
        if !self.conectado {
            return Err("Banco SQLite nao aberto".to_string());
        }
        println!("[SQLite] Executando: {}", sql);
        Ok(ResultadoConsulta {
            colunas: vec!["resultado".into()],
            linhas: vec![vec!["ok".into()]],
        })
    }

    fn desconectar(&mut self) -> Result<(), String> {
        println!("[SQLite] Fechando banco...");
        self.conectado = false;
        Ok(())
    }

    fn nome_driver(&self) -> &str {
        "SQLite"
    }
}

/// Fabrica de conexoes de banco de dados
pub fn criar_conexao(url: &str) -> Result<Box<dyn ConexaoBanco>, String> {
    if url.starts_with("postgres://") || url.starts_with("postgresql://") {
        Ok(Box::new(ConexaoPostgres {
            url: url.to_string(),
            conectado: false,
        }))
    } else if url.starts_with("sqlite://") || url.ends_with(".db") {
        let caminho = url.strip_prefix("sqlite://").unwrap_or(url);
        Ok(Box::new(ConexaoSqlite {
            caminho: caminho.to_string(),
            conectado: false,
        }))
    } else {
        Err(format!("Protocolo de banco nao suportado: {}", url))
    }
}

fn main() {
    let urls = vec![
        "postgres://localhost/meu_app",
        "sqlite://dados.db",
    ];

    for url in urls {
        let mut conexao = criar_conexao(url).expect("Falha ao criar conexao");
        conexao.conectar().expect("Falha ao conectar");

        println!("Driver: {}", conexao.nome_driver());

        let resultado = conexao
            .executar("SELECT * FROM usuarios LIMIT 5")
            .expect("Falha na consulta");

        println!("Colunas: {:?}", resultado.colunas);
        println!("Linhas: {:?}\n", resultado.linhas);

        conexao.desconectar().expect("Falha ao desconectar");
    }
}

Quando Usar

  • Voce nao sabe em tempo de compilacao qual tipo concreto criar (decisao em tempo de execucao)
  • Precisa desacoplar a criacao do uso do objeto
  • Novos tipos podem ser adicionados sem modificar o codigo existente
  • Plugins ou extensoes que registram novos tipos em tempo de execucao
  • Configuracao externa determina qual implementacao usar (arquivo de config, variavel de ambiente)

Quando NAO Usar

  • Apenas um ou dois tipos concretos que nunca mudarao - use construtores diretos
  • Performance critica onde a indirection de Box<dyn Trait> e inaceitavel - use enums ou genericos
  • O tipo e conhecido em tempo de compilacao - use genericos em vez de trait objects
// Se o tipo e conhecido estaticamente, genericos sao melhores:
fn processar<P: Parser>(parser: P, dados: &str) -> Result<Valor, ParserError> {
    parser.parse(dados)
}

// Em vez de:
fn processar(parser: Box<dyn Parser>, dados: &str) -> Result<Valor, ParserError> {
    parser.parse(dados)
}

Variacoes em Rust

1. Factory Function (funcao livre)

A forma mais simples e idiomatica:

pub fn criar_parser(formato: &str) -> Box<dyn Parser> {
    match formato {
        "json" => Box::new(JsonParser),
        "toml" => Box::new(TomlParser),
        _ => panic!("formato desconhecido: {}", formato),
    }
}

2. Factory com Closures

Para fabricas customizaveis sem definir novas structs:

type FabricaFn = Box<dyn Fn(&str) -> Box<dyn Parser>>;

pub struct RegistroFabricas {
    fabricas: HashMap<String, FabricaFn>,
}

impl RegistroFabricas {
    pub fn registrar(&mut self, ext: &str, fabrica: FabricaFn) {
        self.fabricas.insert(ext.to_string(), fabrica);
    }

    pub fn criar(&self, ext: &str, config: &str) -> Option<Box<dyn Parser>> {
        self.fabricas.get(ext).map(|f| f(config))
    }
}

3. Factory via Associated Function em Trait

Usando metodos associados no trait:

pub trait Serializador {
    /// Factory method definido no proprio trait
    fn criar(formato: &str) -> Box<dyn Serializador>
    where
        Self: Sized;

    fn serializar(&self, dados: &Valor) -> String;
}

Padroes Relacionados

  • Builder - Constroi objetos complexos passo a passo; Factory cria de uma so vez
  • Strategy - Muito similar ao Factory com traits; a diferenca e a intencao
  • Prototype - Cria objetos clonando um prototipo em vez de usar uma fabrica
  • Singleton - Factory pode retornar sempre a mesma instancia (Singleton)

Conclusao

O Factory Method em Rust se beneficia enormemente do sistema de traits, que substitui a heranca de classes das linguagens orientadas a objetos. A escolha entre trait objects (Box<dyn Trait>) e enums depende do seu caso de uso: trait objects oferecem extensibilidade em tempo de execucao, enquanto enums oferecem melhor performance e verificacao exaustiva pelo compilador. Em muitos projetos Rust reais, a “fabrica” e simplesmente uma funcao livre que retorna Box<dyn Trait>, demonstrando que nem sempre e necessario criar estruturas complexas para aplicar o padrao.