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.