Builder Pattern em Rust

Aprenda o padrão Builder em Rust: encadeamento de métodos, campos opcionais com Option<T>, validação em tempo de compilação com type-state e exemplos práticos completos.

Introdução

O Builder Pattern (padrão construtor) é provavelmente o padrão de projeto mais utilizado em Rust. Enquanto em linguagens como Java e C# ele é apenas uma conveniência, em Rust ele resolve um problema fundamental: como construir structs complexas com muitos campos opcionais de forma ergonômica e segura.

Rust não possui sobrecarga de construtores, parâmetros nomeados ou valores padrão em argumentos de função. O Builder preenche todas essas lacunas, oferecendo uma API fluente que guia o desenvolvedor na construção de objetos complexos.


Problema

Imagine que você precisa construir uma requisição HTTP. Uma requisição possui dezenas de campos: URL, método, cabeçalhos, corpo, timeout, política de redirecionamento, certificados TLS, e muito mais. A maioria desses campos é opcional e possui valores padrão sensatos.

Sem o Builder, você teria algo assim:

// Isso é terrível - muitos parâmetros, fácil de confundir a ordem
let req = HttpRequest::new(
    "https://api.exemplo.com/dados",
    Method::GET,
    None,           // corpo
    None,           // timeout
    true,           // seguir redirecionamentos?
    None,           // máximo de redirecionamentos
    HashMap::new(), // cabeçalhos
    None,           // certificado TLS
    false,          // verificar SSL?
);

Esse código é frágil, difícil de ler e propenso a erros. Trocar a ordem de dois None pode causar bugs sutis que o compilador não detecta.


Solucao em Rust

Versao Simples: Builder Classico

A forma mais comum do Builder em Rust usa encadeamento de métodos com self:

/// Representa uma requisicao HTTP completa
#[derive(Debug, Clone)]
pub struct HttpRequest {
    url: String,
    method: Method,
    headers: Vec<(String, String)>,
    body: Option<Vec<u8>>,
    timeout_ms: u64,
    follow_redirects: bool,
    max_redirects: u32,
}

#[derive(Debug, Clone)]
pub enum Method {
    Get,
    Post,
    Put,
    Delete,
    Patch,
}

/// Builder para construir HttpRequest passo a passo
#[derive(Debug)]
pub struct HttpRequestBuilder {
    url: String,
    method: Method,
    headers: Vec<(String, String)>,
    body: Option<Vec<u8>>,
    timeout_ms: u64,
    follow_redirects: bool,
    max_redirects: u32,
}

impl HttpRequestBuilder {
    /// Cria um novo builder com a URL obrigatoria
    pub fn new(url: impl Into<String>) -> Self {
        Self {
            url: url.into(),
            method: Method::Get,
            headers: Vec::new(),
            body: None,
            timeout_ms: 30_000, // 30 segundos padrao
            follow_redirects: true,
            max_redirects: 10,
        }
    }

    /// Define o metodo HTTP
    pub fn method(mut self, method: Method) -> Self {
        self.method = method;
        self
    }

    /// Adiciona um cabecalho a requisicao
    pub fn header(mut self, nome: impl Into<String>, valor: impl Into<String>) -> Self {
        self.headers.push((nome.into(), valor.into()));
        self
    }

    /// Define o corpo da requisicao
    pub fn body(mut self, body: impl Into<Vec<u8>>) -> Self {
        self.body = Some(body.into());
        self
    }

    /// Define o corpo como texto JSON
    pub fn json(self, json: impl Into<String>) -> Self {
        let json_str = json.into();
        self.header("Content-Type", "application/json")
            .body(json_str.into_bytes())
    }

    /// Define o timeout em milissegundos
    pub fn timeout_ms(mut self, ms: u64) -> Self {
        self.timeout_ms = ms;
        self
    }

    /// Controla se deve seguir redirecionamentos
    pub fn follow_redirects(mut self, seguir: bool) -> Self {
        self.follow_redirects = seguir;
        self
    }

    /// Constroi a requisicao final
    pub fn build(self) -> Result<HttpRequest, String> {
        // Validacao em tempo de execucao
        if self.url.is_empty() {
            return Err("URL nao pode ser vazia".to_string());
        }

        // POST/PUT/PATCH sem corpo gera aviso (mas permitimos)
        Ok(HttpRequest {
            url: self.url,
            method: self.method,
            headers: self.headers,
            body: self.body,
            timeout_ms: self.timeout_ms,
            follow_redirects: self.follow_redirects,
            max_redirects: self.max_redirects,
        })
    }
}

// Uso:
fn main() {
    let requisicao = HttpRequestBuilder::new("https://api.exemplo.com/usuarios")
        .method(Method::Post)
        .header("Authorization", "Bearer meu-token-secreto")
        .json(r#"{"nome": "Maria", "email": "maria@exemplo.com"}"#)
        .timeout_ms(5_000)
        .build()
        .expect("Falha ao construir requisicao");

    println!("Requisicao construida: {:?}", requisicao);
}

Versao Avancada: Type-State Builder

A versao type-state usa o sistema de tipos para garantir em tempo de compilacao que campos obrigatorios foram preenchidos. Codigo que esquece um campo obrigatorio simplesmente nao compila.

use std::marker::PhantomData;

/// Marcadores de tipo para os estados do builder
mod estado {
    /// Campo ainda nao foi definido
    pub struct Ausente;
    /// Campo ja foi definido
    pub struct Presente;
}

/// Builder com validacao em tempo de compilacao
/// Os genericos rastreiam quais campos obrigatorios foram preenchidos
pub struct ConfigBuilder<HostState, PortaState> {
    host: Option<String>,
    porta: Option<u16>,
    max_conexoes: u32,
    timeout_segundos: u64,
    tls_habilitado: bool,
    nome_banco: Option<String>,
    // PhantomData para os estados (custo zero em tempo de execucao)
    _host: PhantomData<HostState>,
    _porta: PhantomData<PortaState>,
}

/// Configuracao final do banco de dados
#[derive(Debug)]
pub struct DatabaseConfig {
    pub host: String,
    pub porta: u16,
    pub max_conexoes: u32,
    pub timeout_segundos: u64,
    pub tls_habilitado: bool,
    pub nome_banco: Option<String>,
}

impl ConfigBuilder<estado::Ausente, estado::Ausente> {
    /// Cria um novo builder — nenhum campo obrigatorio preenchido ainda
    pub fn new() -> Self {
        Self {
            host: None,
            porta: None,
            max_conexoes: 10,
            timeout_segundos: 30,
            tls_habilitado: false,
            nome_banco: None,
            _host: PhantomData,
            _porta: PhantomData,
        }
    }
}

impl<H, P> ConfigBuilder<H, P> {
    /// Define o numero maximo de conexoes (campo opcional)
    pub fn max_conexoes(mut self, n: u32) -> Self {
        self.max_conexoes = n;
        self
    }

    /// Define o timeout em segundos (campo opcional)
    pub fn timeout_segundos(mut self, s: u64) -> Self {
        self.timeout_segundos = s;
        self
    }

    /// Habilita ou desabilita TLS (campo opcional)
    pub fn tls(mut self, habilitado: bool) -> Self {
        self.tls_habilitado = habilitado;
        self
    }

    /// Define o nome do banco de dados (campo opcional)
    pub fn nome_banco(mut self, nome: impl Into<String>) -> Self {
        self.nome_banco = Some(nome.into());
        self
    }
}

impl<P> ConfigBuilder<estado::Ausente, P> {
    /// Define o host (campo OBRIGATORIO)
    /// Note: retorna ConfigBuilder<Presente, P> — o estado muda!
    pub fn host(self, host: impl Into<String>) -> ConfigBuilder<estado::Presente, P> {
        ConfigBuilder {
            host: Some(host.into()),
            porta: self.porta,
            max_conexoes: self.max_conexoes,
            timeout_segundos: self.timeout_segundos,
            tls_habilitado: self.tls_habilitado,
            nome_banco: self.nome_banco,
            _host: PhantomData,
            _porta: PhantomData,
        }
    }
}

impl<H> ConfigBuilder<H, estado::Ausente> {
    /// Define a porta (campo OBRIGATORIO)
    /// Note: retorna ConfigBuilder<H, Presente> — o estado muda!
    pub fn porta(self, porta: u16) -> ConfigBuilder<H, estado::Presente> {
        ConfigBuilder {
            host: self.host,
            porta: Some(porta),
            max_conexoes: self.max_conexoes,
            timeout_segundos: self.timeout_segundos,
            tls_habilitado: self.tls_habilitado,
            nome_banco: self.nome_banco,
            _host: PhantomData,
            _porta: PhantomData,
        }
    }
}

// build() SO esta disponivel quando AMBOS os campos obrigatorios estao presentes
impl ConfigBuilder<estado::Presente, estado::Presente> {
    /// Constroi a configuracao final
    /// So pode ser chamado quando host E porta foram definidos
    pub fn build(self) -> DatabaseConfig {
        DatabaseConfig {
            host: self.host.expect("host garantido pelo type-state"),
            porta: self.porta.expect("porta garantida pelo type-state"),
            max_conexoes: self.max_conexoes,
            timeout_segundos: self.timeout_segundos,
            tls_habilitado: self.tls_habilitado,
            nome_banco: self.nome_banco,
        }
    }
}

fn main() {
    // Isso COMPILA - todos os campos obrigatorios presentes
    let config = ConfigBuilder::new()
        .host("localhost")
        .porta(5432)
        .max_conexoes(20)
        .tls(true)
        .nome_banco("meu_app")
        .build();

    println!("Config: {:?}", config);

    // Isso NAO COMPILA - porta nao foi definida
    // let config_invalido = ConfigBuilder::new()
    //     .host("localhost")
    //     .max_conexoes(20)
    //     .build(); // ERRO: metodo `build` nao encontrado para ConfigBuilder<Presente, Ausente>
}

Diagrama

+--------------------------------------------------+
|              Fluxo do Builder Pattern             |
+--------------------------------------------------+

                  Builder::new()
                       |
                       v
              +----------------+
              |   Builder      |
              |  (estado       |
              |   inicial)     |
              +----------------+
                       |
          .host("...")  .porta(5432)  .tls(true)
                       |
                       v
              +----------------+
              |   Builder      |
              |  (campos       |
              |   preenchidos) |
              +----------------+
                       |
                   .build()
                       |
                       v
              +----------------+
              |   Produto      |
              |  (imutavel,    |
              |   validado)    |
              +----------------+


  TYPE-STATE BUILDER (transicoes de tipo):

  ConfigBuilder<Ausente, Ausente>
         |
         | .host("localhost")
         v
  ConfigBuilder<Presente, Ausente>
         |
         | .porta(5432)
         v
  ConfigBuilder<Presente, Presente>  <-- build() disponivel APENAS aqui
         |
         | .build()
         v
     DatabaseConfig

Exemplo do Mundo Real

Um construtor de configuracao para uma aplicacao web completa:

use std::collections::HashMap;
use std::path::PathBuf;
use std::net::SocketAddr;

/// Configuracao completa de uma aplicacao web
#[derive(Debug, Clone)]
pub struct AppConfig {
    pub endereco: SocketAddr,
    pub workers: usize,
    pub banco_url: String,
    pub redis_url: Option<String>,
    pub log_level: LogLevel,
    pub cors_origens: Vec<String>,
    pub limites_upload_mb: u64,
    pub diretorio_estatico: Option<PathBuf>,
    pub segredos: HashMap<String, String>,
    pub ambiente: Ambiente,
}

#[derive(Debug, Clone)]
pub enum LogLevel {
    Trace,
    Debug,
    Info,
    Warn,
    Error,
}

#[derive(Debug, Clone)]
pub enum Ambiente {
    Desenvolvimento,
    Teste,
    Producao,
}

#[derive(Debug)]
pub struct AppConfigBuilder {
    endereco: SocketAddr,
    workers: usize,
    banco_url: Option<String>,
    redis_url: Option<String>,
    log_level: LogLevel,
    cors_origens: Vec<String>,
    limites_upload_mb: u64,
    diretorio_estatico: Option<PathBuf>,
    segredos: HashMap<String, String>,
    ambiente: Ambiente,
}

impl AppConfigBuilder {
    pub fn new() -> Self {
        let num_cpus = std::thread::available_parallelism()
            .map(|n| n.get())
            .unwrap_or(4);

        Self {
            endereco: "127.0.0.1:8080".parse().unwrap(),
            workers: num_cpus,
            banco_url: None,
            redis_url: None,
            log_level: LogLevel::Info,
            cors_origens: Vec::new(),
            limites_upload_mb: 10,
            diretorio_estatico: None,
            segredos: HashMap::new(),
            ambiente: Ambiente::Desenvolvimento,
        }
    }

    /// Configura para producao com padroes seguros
    pub fn producao(mut self) -> Self {
        self.ambiente = Ambiente::Producao;
        self.log_level = LogLevel::Warn;
        self.endereco = "0.0.0.0:443".parse().unwrap();
        self
    }

    pub fn endereco(mut self, addr: impl Into<String>) -> Self {
        self.endereco = addr.into().parse().expect("endereco invalido");
        self
    }

    pub fn workers(mut self, n: usize) -> Self {
        self.workers = n;
        self
    }

    pub fn banco_url(mut self, url: impl Into<String>) -> Self {
        self.banco_url = Some(url.into());
        self
    }

    pub fn redis_url(mut self, url: impl Into<String>) -> Self {
        self.redis_url = Some(url.into());
        self
    }

    pub fn log_level(mut self, level: LogLevel) -> Self {
        self.log_level = level;
        self
    }

    pub fn cors_origem(mut self, origem: impl Into<String>) -> Self {
        self.cors_origens.push(origem.into());
        self
    }

    pub fn limite_upload_mb(mut self, mb: u64) -> Self {
        self.limites_upload_mb = mb;
        self
    }

    pub fn diretorio_estatico(mut self, dir: impl Into<PathBuf>) -> Self {
        self.diretorio_estatico = Some(dir.into());
        self
    }

    pub fn segredo(mut self, chave: impl Into<String>, valor: impl Into<String>) -> Self {
        self.segredos.insert(chave.into(), valor.into());
        self
    }

    /// Constroi a configuracao, validando campos obrigatorios
    pub fn build(self) -> Result<AppConfig, Vec<String>> {
        let mut erros = Vec::new();

        if self.banco_url.is_none() {
            erros.push("URL do banco de dados e obrigatoria".to_string());
        }

        if self.workers == 0 {
            erros.push("Numero de workers deve ser maior que zero".to_string());
        }

        if matches!(self.ambiente, Ambiente::Producao) && self.segredos.is_empty() {
            erros.push("Producao requer ao menos um segredo configurado".to_string());
        }

        if !erros.is_empty() {
            return Err(erros);
        }

        Ok(AppConfig {
            endereco: self.endereco,
            workers: self.workers,
            banco_url: self.banco_url.unwrap(),
            redis_url: self.redis_url,
            log_level: self.log_level,
            cors_origens: self.cors_origens,
            limites_upload_mb: self.limites_upload_mb,
            diretorio_estatico: self.diretorio_estatico,
            segredos: self.segredos,
            ambiente: self.ambiente,
        })
    }
}

fn main() {
    // Builder para desenvolvimento local
    let config_dev = AppConfigBuilder::new()
        .banco_url("postgres://localhost/meu_app_dev")
        .redis_url("redis://localhost:6379")
        .cors_origem("http://localhost:3000")
        .log_level(LogLevel::Debug)
        .diretorio_estatico("./static")
        .build()
        .expect("Configuracao de dev invalida");

    println!("Dev: {:?}\n", config_dev);

    // Builder para producao
    let config_prod = AppConfigBuilder::new()
        .producao()
        .banco_url("postgres://db.prod.interno/meu_app")
        .redis_url("redis://cache.prod.interno:6379")
        .cors_origem("https://meuapp.com.br")
        .cors_origem("https://www.meuapp.com.br")
        .segredo("JWT_SECRET", "chave-super-secreta-producao")
        .segredo("API_KEY", "chave-api-externa")
        .limite_upload_mb(50)
        .workers(16)
        .build()
        .expect("Configuracao de producao invalida");

    println!("Prod: {:?}", config_prod);
}

Quando Usar

  • Structs com mais de 3-4 campos, especialmente se varios sao opcionais
  • APIs publicas de bibliotecas onde ergonomia e importante
  • Objetos imutaveis que devem ser totalmente configurados antes do uso
  • Validacao complexa que depende da combinacao de varios campos
  • Configuracoes de aplicacao, conexoes, requisicoes

Quando NAO Usar

  • Structs simples com poucos campos - use construtores new() diretos
  • Structs onde todos os campos sao obrigatorios - a construcao direta e mais clara
  • Tipos Copy pequenos como Point { x: f64, y: f64 } - Builder e overkill
  • Quando Default + modificacao funciona - considere #[derive(Default)] + atribuicao
// Para structs simples, Default pode ser suficiente
#[derive(Debug, Default)]
struct OpcoesBusca {
    pagina: u32,
    por_pagina: u32,
    ordenar_por: Option<String>,
}

let opcoes = OpcoesBusca {
    pagina: 2,
    por_pagina: 50,
    ..Default::default()
};

Variacoes em Rust

1. Builder com &mut self (reutilizavel)

impl MeuBuilder {
    pub fn campo(&mut self, valor: String) -> &mut Self {
        self.campo = valor;
        self
    }

    // Permite chamar build() varias vezes
    pub fn build(&self) -> MeuTipo {
        MeuTipo { campo: self.campo.clone() }
    }
}

2. Builder com derive_builder (macro)

// Cargo.toml: derive_builder = "0.12"
use derive_builder::Builder;

#[derive(Builder, Debug)]
#[builder(setter(into))]
pub struct Servidor {
    host: String,
    porta: u16,
    #[builder(default = "4")]
    workers: usize,
    #[builder(default)]
    tls: bool,
}

// Uso automatico:
let srv = ServidorBuilder::default()
    .host("localhost")
    .porta(8080u16)
    .build()
    .unwrap();

3. Builder com bon (macro moderna)

// Cargo.toml: bon = "3"
use bon::bon;

#[derive(Debug)]
pub struct Conexao {
    host: String,
    porta: u16,
    pool_size: u32,
}

#[bon]
impl Conexao {
    #[builder]
    fn new(host: String, porta: u16, #[builder(default = 5)] pool_size: u32) -> Self {
        Self { host, porta, pool_size }
    }
}

// Gera builder automaticamente:
let conn = Conexao::builder()
    .host("localhost".to_string())
    .porta(5432)
    .build();

Padroes Relacionados

  • Factory Method - O Builder constroi objetos complexos passo a passo; o Factory cria objetos de uma so vez
  • Prototype - Pode-se combinar Builder com Clone para criar variacoes de um template
  • Singleton - Builders sao frequentemente usados para construir a instancia unica de um Singleton

Conclusao

O Builder e o padrao mais natural e idiomatico em Rust. Diferente de linguagens com construtores sobrecarregados e parametros nomeados, Rust precisa do Builder para oferecer APIs ergonomicas. A versao type-state aproveita o sistema de tipos de Rust para mover validacoes do tempo de execucao para o tempo de compilacao, eliminando classes inteiras de bugs. Seja na forma simples com encadeamento de metodos ou na forma avancada com type-state, o Builder deve ser a primeira ferramenta no cinto de utilidades de todo desenvolvedor Rust.