Default e o padrão Builder
Nesta aula, você aprenderá a usar o trait Default do Rust para fornecer valores padrão a tipos e como aplicar o padrão de projeto Builder para construir objetos complexos de forma ergonômica e legível, com exemplos práticos e exercícios.
Nesta aula, vamos explorar dois conceitos fundamentais para a construção de tipos em Rust: o trait Default e o padrão de projeto Builder. O trait Default permite que você defina valores padrão para seus tipos, facilitando a criação de instâncias sem a necessidade de especificar todos os campos. Já o padrão Builder é uma técnica de design que separa a construção de um objeto de sua representação, permitindo que você configure passo a passo os atributos desejados de forma clara e fluente.
Combinando esses dois elementos, você poderá criar APIs ergonômicas e expressivas, onde a construção de objetos complexos se torna intuitiva e menos propensa a erros. Vamos mergulhar nos detalhes de cada um e ver como aplicá-los no contexto da linguagem Rust.
trait Default
O trait Default é um dos traits mais básicos e úteis da biblioteca padrão do Rust. Ele define um único método: fn default() -> Self. Qualquer tipo que implemente Default fornece um valor padrão, que pode ser usado em diversas situações, como quando você precisa inicializar uma variável sem especificar todos os campos.
Muitos tipos da biblioteca padrão já implementam Default, como números inteiros (0), floats (0.0), bool (false), String (String::new()), Vec (Vec::new()), entre outros. Para seus próprios tipos, você pode implementar o trait manualmente ou derivá-lo usando #[derive(Default)] quando todos os campos implementam Default.
Vamos ver um exemplo de implementação manual e derivada:
#[derive(Debug, Default)]
struct Config {
nome: String,
versao: u32,
otimizacao: bool,
}
impl Default for Config {
fn default() -> Self {
Self {
nome: String::from("meu_app"),
versao: 1,
otimizacao: true,
}
}
}
fn main() {
let config = Config::default();
println!("{:?}", config);
}
No exemplo, derivamos Default e também implementamos manualmente para mostrar as duas abordagens. Quando você usa #[derive(Default)], todos os campos são inicializados com seus valores padrão (para String é vazio, para u32 é 0, para bool é false). A implementação manual permite personalizar os valores padrão.
O trait Default é frequentemente usado em conjunto com ..Default::default() na sintaxe de atualização de struct, para preencher os campos não especificados. Isso é muito útil em builders e em APIs de configuração.
Builder pattern
O padrão Builder é um padrão de projeto criacional que permite construir objetos complexos passo a passo. Em Rust, ele é frequentemente implementado criando uma struct Builder separada que acumula os parâmetros e, no final, um método build() que valida e constrói o objeto final. Isso é especialmente útil quando um tipo tem muitos campos, alguns opcionais, e você quer evitar construtores com muitos argumentos.
Vamos criar um exemplo de um tipo Pessoa com campos obrigatórios e opcionais:
#[derive(Debug)]
struct Pessoa {
nome: String,
idade: u8,
email: Option,
ativo: bool,
}
struct PessoaBuilder {
nome: String,
idade: u8,
email: Option,
ativo: bool,
}
impl PessoaBuilder {
fn novo(nome: impl Into) -> Self {
Self {
nome: nome.into(),
idade: 0,
email: None,
ativo: false,
}
}
fn idade(mut self, idade: u8) -> Self {
self.idade = idade;
self
}
fn email(mut self, email: impl Into) -> Self {
self.email = Some(email.into());
self
}
fn ativo(mut self, ativo: bool) -> Self {
self.ativo = ativo;
self
}
fn build(self) -> Pessoa {
Pessoa {
nome: self.nome,
idade: self.idade,
email: self.email,
ativo: self.ativo,
}
}
}
fn main() {
let pessoa = PessoaBuilder::novo("Maria")
.idade(30)
.email("maria@example.com")
.ativo(true)
.build();
println!("{:?}", pessoa);
}
Observe que cada método setter consome self e retorna Self, permitindo encadeamento. O método build() consome o builder e produz a struct final. Essa abordagem é muito comum em Rust e é usada por crates populares como reqwest para construir requisições HTTP, ou clap para definir argumentos de linha de comando.
Uma variação é usar &mut self em vez de self para permitir reutilização do builder, mas o consumo é mais idiomático e evita cópias desnecessárias.
Construção ergonômica
A combinação do trait Default com o padrão Builder pode tornar a construção de objetos ainda mais ergonômica. Por exemplo, você pode inicializar o builder com valores padrão e permitir que o usuário sobrescreva apenas os campos desejados. Além disso, o uso de Default no método build() pode simplificar a criação quando muitos campos têm valores padrão.
Vamos melhorar o exemplo anterior usando Default para inicializar o builder:
#[derive(Debug, Default)]
struct Pessoa {
nome: String,
idade: u8,
email: Option,
ativo: bool,
}
#[derive(Default)]
struct PessoaBuilder {
nome: String,
idade: u8,
email: Option,
ativo: bool,
}
impl PessoaBuilder {
fn novo() -> Self {
Self::default()
}
fn nome(mut self, nome: impl Into) -> Self {
self.nome = nome.into();
self
}
fn idade(mut self, idade: u8) -> Self {
self.idade = idade;
self
}
fn email(mut self, email: impl Into) -> Self {
self.email = Some(email.into());
self
}
fn ativo(mut self, ativo: bool) -> Self {
self.ativo = ativo;
self
}
fn build(self) -> Pessoa {
Pessoa {
nome: self.nome,
idade: self.idade,
email: self.email,
ativo: self.ativo,
}
}
}
fn main() {
let pessoa = PessoaBuilder::novo()
.nome("João")
.idade(25)
.build();
println!("{:?}", pessoa);
}
Nesta versão, o builder usa #[derive(Default)], então todos os campos começam com valores padrão. O usuário só precisa chamar os métodos para os campos que deseja alterar. Isso reduz a verbosidade e torna o código mais limpo.
Outra técnica ergonômica é usar o padrão consuming builder com build() que valida os dados, retornando Result em caso de erro. Isso é comum em bibliotecas onde a configuração precisa ser verificada.
Exemplos
Vamos ver um exemplo mais completo de um builder para configurar um servidor HTTP simples. Usaremos Default para fornecer configurações padrão e o builder para personalizar:
#[derive(Debug)]
struct ServerConfig {
host: String,
port: u16,
workers: usize,
timeout: u64,
enable_log: bool,
}
impl Default for ServerConfig {
fn default() -> Self {
Self {
host: String::from("127.0.0.1"),
port: 8080,
workers: 4,
timeout: 30,
enable_log: true,
}
}
}
struct ServerConfigBuilder {
config: ServerConfig,
}
impl ServerConfigBuilder {
fn new() -> Self {
Self { config: ServerConfig::default() }
}
fn host(mut self, host: impl Into) -> Self {
self.config.host = host.into();
self
}
fn port(mut self, port: u16) -> Self {
self.config.port = port;
self
}
fn workers(mut self, workers: usize) -> Self {
self.config.workers = workers;
self
}
fn timeout(mut self, timeout: u64) -> Self {
self.config.timeout = timeout;
self
}
fn enable_log(mut self, enable: bool) -> Self {
self.config.enable_log = enable;
self
}
fn build(self) -> ServerConfig {
self.config
}
}
fn main() {
let config = ServerConfigBuilder::new()
.host("0.0.0.0")
.port(3000)
.workers(8)
.build();
println!("{:?}", config);
}
Neste exemplo, o builder internamente mantém uma ServerConfig que já é inicializada com Default. Cada método setter modifica um campo e retorna self. O método build() simplesmente retorna a configuração final. Isso é um padrão comum e eficiente.
Outro exemplo é o uso de Default em funções genéricas. Por exemplo, você pode ter uma função que aceita um parâmetro opcional, usando Default para fornecer um valor padrão:
fn processar<T: Default>(valor: Option<T>) -> T {
valor.unwrap_or_default()
}
fn main() {
let x: u32 = processar(None);
let y: String = processar(Some(String::from("hi")));
println!("x = {}, y = {}", x, y);
}
Aqui, unwrap_or_default() usa o trait Default para retornar um valor padrão se o Option for None. Isso é muito útil em código genérico.
Referências
- Trait Default - Documentação oficial do Rust
- O padrão Builder no Rust Book
- Builder Pattern - Rust Design Patterns
- Derive Default - Referência da linguagem
- API Guidelines - C-DEFAULT
Exercícios
- Crie uma struct
Pontocom camposxeydo tipof64e implemente o traitDefaultpara que o valor padrão seja(0.0, 0.0). Teste comPonto::default(). - Implemente um builder para uma struct
Retangulocom camposlarguraealtura(u32). O builder deve ter métodoslargura()ealtura()e um métodobuild()que retorna a struct. Use valores padrão 1 para ambos. - Use o trait
Defaultem uma função genérica que retorna o valor padrão de um tipoTquando umOptionéNone. Teste comu32eString. - Modifique o builder de
Pessoa(do exemplo) para usar#[derive(Default)]no builder e adicione um métodobuild()que retornaResult<Pessoa, String>se o nome estiver vazio, para validar os dados. - Crie um exemplo que combine
Defaulte builder para configurar uma conexão de banco de dados com campos:url(String),pool_size(u32),timeout(u64) essl(bool). Use valores padrão sensatos e permita sobrescrever qualquer campo.
#[derive(Debug)]
struct Ponto {
x: f64,
y: f64,
}
impl Default for Ponto {
fn default() -> Self {
Self { x: 0.0, y: 0.0 }
}
}
fn main() {
let p = Ponto::default();
println!("{:?}", p);
}
#[derive(Debug)]
struct Retangulo {
largura: u32,
altura: u32,
}
struct RetanguloBuilder {
largura: u32,
altura: u32,
}
impl RetanguloBuilder {
fn new() -> Self {
Self { largura: 1, altura: 1 }
}
fn largura(mut self, valor: u32) -> Self {
self.largura = valor;
self
}
fn altura(mut self, valor: u32) -> Self {
self.altura = valor;
self
}
fn build(self) -> Retangulo {
Retangulo { largura: self.largura, altura: self.altura }
}
}
fn main() {
let r = RetanguloBuilder::new().largura(5).build();
println!("{:?}", r);
}
fn obter_valor<T: Default>(opt: Option<T>) -> T {
opt.unwrap_or_default()
}
fn main() {
let num: u32 = obter_valor(None);
let texto: String = obter_valor(Some(String::from("oi")));
println!("{}, {}", num, texto);
}
#[derive(Debug, Default)]
struct Pessoa {
nome: String,
idade: u8,
email: Option<String>,
ativo: bool,
}
#[derive(Default)]
struct PessoaBuilder {
nome: String,
idade: u8,
email: Option<String>,
ativo: bool,
}
impl PessoaBuilder {
fn novo() -> Self {
Self::default()
}
fn nome(mut self, nome: impl Into<String>) -> Self {
self.nome = nome.into();
self
}
fn idade(mut self, idade: u8) -> Self {
self.idade = idade;
self
}
fn email(mut self, email: impl Into<String>) -> Self {
self.email = Some(email.into());
self
}
fn ativo(mut self, ativo: bool) -> Self {
self.ativo = ativo;
self
}
fn build(self) -> Result<Pessoa, String> {
if self.nome.is_empty() {
Err(String::from("O nome é obrigatório"))
} else {
Ok(Pessoa {
nome: self.nome,
idade: self.idade,
email: self.email,
ativo: self.ativo,
})
}
}
}
fn main() {
let pessoa = PessoaBuilder::novo().nome("Ana").build();
println!("{:?}", pessoa);
}
#[derive(Debug)]
struct DbConfig {
url: String,
pool_size: u32,
timeout: u64,
ssl: bool,
}
impl Default for DbConfig {
fn default() -> Self {
Self {
url: String::from("localhost:5432"),
pool_size: 10,
timeout: 30,
ssl: false,
}
}
}
struct DbConfigBuilder {
config: DbConfig,
}
impl DbConfigBuilder {
fn new() -> Self {
Self { config: DbConfig::default() }
}
fn url(mut self, url: impl Into<String>) -> Self {
self.config.url = url.into();
self
}
fn pool_size(mut self, size: u32) -> Self {
self.config.pool_size = size;
self
}
fn timeout(mut self, timeout: u64) -> Self {
self.config.timeout = timeout;
self
}
fn ssl(mut self, ssl: bool) -> Self {
self.config.ssl = ssl;
self
}
fn build(self) -> DbConfig {
self.config
}
}
fn main() {
let cfg = DbConfigBuilder::new().url("db.example.com").ssl(true).build();
println!("{:?}", cfg);
}
Boas práticas e observações finais
Ao usar Default e builders em Rust, lembre-se de que o padrão Builder é especialmente útil quando você tem muitos parâmetros opcionais ou quando a construção envolve validação. Sempre que possível, derive Default para reduzir código boilerplate, mas não hesite em implementar manualmente quando precisar de valores padrão personalizados.
Outra dica: em bibliotecas públicas, considere usar o padrão builder para manter a compatibilidade semântica, pois permite adicionar novos campos sem quebrar o código existente. Além disso, a combinação com Default pode simplificar a inicialização, mas cuidado para não esconder erros de configuração: use Result em build() quando houver pré-condições.