Na construção de software, frequentemente precisamos criar instâncias de tipos com valorespadrão ou com configurações customizadas. Rust oferece o trait Default para fornecer valores padrão e o padrão Builder para construir objetos passo a passo, de forma flexível e ergonômica. Nesta aula, vamos explorar ambos, entendendo suas aplicações e como combiná-los para escrever código mais limpo e expressivo.

O padrão Builder é amplamente utilizado quando um tipo tem muitos campos opcionais ou quando a ordem de configuração importa. Combinado com Default, podemos simplificar a implementação e oferecer uma experiência agradável para quem usa nossa API. Vamos mergulhar em exemplos práticos e ver como essas técnicas se complementam.

Trait Default

O trait Default é definido na biblioteca padrão do Rust e fornece um método default() que retorna uma nova instância do tipo com valores padrão. É uma forma de fornecer um valor inicial sensato para um tipo, sem exigir que o usuário especifique todos os campos.

Por exemplo, tipos numéricos têm Default retornando zero, String retorna uma string vazia, e Vec retorna um vetor vazio. Para nossos próprios tipos, podemos implementar Default manualmente ou derivá-lo com #[derive(Default)] se todos os campos implementarem Default.

#[derive(Debug, Default)]
struct Config {
    host: String,
    port: u16,
    timeout: u32,
}

fn main() {
    let cfg = Config::default();
    println!("{:?}", cfg); // Config { host: "", port: 0, timeout: 0 }
}

Derivar Default é conveniente, mas às vezes queremos valores padrão diferentes de zero ou vazio. Nesses casos, implementamos manualmente. Por exemplo, um servidor pode ter um host padrão "localhost" e porta 8080.

struct ServerConfig {
    host: String,
    port: u16,
}

impl Default for ServerConfig {
    fn default() -> Self {
        Self {
            host: String::from("localhost"),
            port: 8080,
        }
    }
}

O trait Default é especialmente útil quando combinado com o padrão Builder, porque permite inicializar o builder com valores padrão e depois modificar apenas os campos desejados.

Builder pattern

O padrão Builder é um padrão de projeto criacional que separa a construção de um objeto complexo de sua representação. Em Rust, é frequentemente implementado com um struct separado que coleta configurações e um método build() que valida e retorna o objeto final.

Isso é útil quando um tipo tem muitos parâmetros, alguns opcionais, e queremos evitar longas listas de argumentos ou múltiplas sobrecargas de construtores. O Builder também permite encadeamento de métodos, tornando o código legível e fluente.

Vamos criar um exemplo de um Car com vários campos opcionais. Primeiro, definimos o struct Car e depois o CarBuilder.

#[derive(Debug, Clone)]
struct Car {
    brand: String,
    model: String,
    year: u16,
    color: String,
    sunroof: bool,
}

struct CarBuilder {
    brand: String,
    model: String,
    year: Option<u16>,
    color: Option<String>,
    sunroof: bool,
}

impl CarBuilder {
    fn new(brand: &str, model: &str) -> Self {
        Self {
            brand: brand.to_string(),
            model: model.to_string(),
            year: None,
            color: None,
            sunroof: false,
        }
    }

    fn year(mut self, year: u16) -> Self {
        self.year = Some(year);
        self
    }

    fn color(mut self, color: &str) -> Self {
        self.color = Some(color.to_string());
        self
    }

    fn sunroof(mut self, sunroof: bool) -> Self {
        self.sunroof = sunroof;
        self
    }

    fn build(self) -> Result<Car, String> {
        let year = self.year.unwrap_or(2024); // valor padrão
        let color = self.color.unwrap_or_else(|| String::from("white"));
        Ok(Car {
            brand: self.brand,
            model: self.model,
            year,
            color,
            sunroof: self.sunroof,
        })
    }
}

fn main() {
    let car = CarBuilder::new("Toyota", "Corolla")
        .year(2022)
        .color("blue")
        .sunroof(true)
        .build()
        .unwrap();
    println!("{:?}", car);
}

O builder permite que o usuário especifique apenas os campos que deseja, e os demais ficam com valores padrão. Isso melhora a legibilidade e evita erros de argumentos trocados.

Construção ergonômica

Podemos tornar a construção ainda mais ergonômica combinando Default com o Builder. Em vez de inicializar manualmente cada campo no builder, podemos começar com Self::default() ou Self::new() que usa valores padrão, e então sobrescrever apenas os campos necessários.

Uma abordagem comum é implementar Default para o builder, preenchendo todos os campos com valores padrão, e depois fornecer métodos que alteram esses campos. Isso reduz a repetição e garante que nenhum campo fique sem valor.

#[derive(Clone)]
struct CarBuilder {
    brand: String,
    model: String,
    year: u16,
    color: String,
    sunroof: bool,
}

impl Default for CarBuilder {
    fn default() -> Self {
        Self {
            brand: "Unknown".to_string(),
            model: "Unknown".to_string(),
            year: 2024,
            color: "white".to_string(),
            sunroof: false,
        }
    }
}

impl CarBuilder {
    fn new() -> Self {
        Self::default()
    }

    fn brand(mut self, brand: &str) -> Self {
        self.brand = brand.to_string();
        self
    }

    // ... outros métodos
    fn build(self) -> Car {
        Car {
            brand: self.brand,
            model: self.model,
            year: self.year,
            color: self.color,
            sunroof: self.sunroof,
        }
    }
}

Outra técnica é usar o padrão builder com tipo fantasma para garantir que campos obrigatórios sejam preenchidos em tempo de compilação. Isso aumenta a segurança, mas adiciona complexidade. Para a maioria dos casos, um builder simples com valores padrão já é suficiente.

Além disso, podemos usar o crate derive_builder para gerar automaticamente o código do builder a partir de um struct, economizando tempo e reduzindo erros. No entanto, é importante entender a implementação manual antes de usar ferramentas automáticas.

Exemplos

Vamos ver um exemplo completo que combina Default e Builder para configurar um servidor HTTP simples. Teremos um struct ServerConfig com vários campos e um ServerBuilder.

use std::time::Duration;

#[derive(Debug, Clone)]
struct ServerConfig {
    host: String,
    port: u16,
    workers: usize,
    timeout: Duration,
    tls: bool,
}

impl Default for ServerConfig {
    fn default() -> Self {
        Self {
            host: "127.0.0.1".to_string(),
            port: 8080,
            workers: 4,
            timeout: Duration::from_secs(30),
            tls: false,
        }
    }
}

#[derive(Clone)]
struct ServerConfigBuilder {
    config: ServerConfig,
}

impl ServerConfigBuilder {
    fn new() -> Self {
        Self { config: ServerConfig::default() }
    }

    fn host(mut self, host: &str) -> Self {
        self.config.host = host.to_string();
        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: Duration) -> Self {
        self.config.timeout = timeout;
        self
    }

    fn tls(mut self, tls: bool) -> Self {
        self.config.tls = tls;
        self
    }

    fn build(self) -> ServerConfig {
        self.config
    }
}

fn main() {
    let config = ServerConfigBuilder::new()
        .host("0.0.0.0")
        .port(3000)
        .tls(true)
        .build();
    println!("{:?}", config);
}

Neste exemplo, o builder começa com os valores padrão e permite sobrescrever apenas os campos desejados. Isso torna a construção clara e flexível.

Outra aplicação comum é em testes, onde precisamos criar muitos objetos com pequenas variações. O Builder facilita a criação de fixtures.

#[derive(Debug, Default)]
struct User {
    name: String,
    age: u8,
    email: String,
}

fn main() {
    // Usando Default
    let user = User::default();
    println!("{:?}", user);

    // Usando um builder simples
    let user = UserBuilder::new()
        .name("Alice")
        .age(30)
        .build();
    println!("{:?}", user);
}

struct UserBuilder {
    name: String,
    age: u8,
    email: String,
}

impl UserBuilder {
    fn new() -> Self {
        Self {
            name: String::new(),
            age: 0,
            email: String::new(),
        }
    }

    fn name(mut self, name: &str) -> Self {
        self.name = name.to_string();
        self
    }

    fn age(mut self, age: u8) -> Self {
        self.age = age;
        self
    }

    fn email(mut self, email: &str) -> Self {
        self.email = email.to_string();
        self
    }

    fn build(self) -> User {
        User {
            name: self.name,
            age: self.age,
            email: self.email,
        }
    }
}

Observe que no builder manual precisamos inicializar todos os campos, mesmo que sejam vazios. Usando Default para o builder, podemos simplificar ainda mais.

Boas práticas e observações finais

  • Use Default quando houver um valor padrão natural para o tipo. Isso torna o código mais conciso e permite combinar com o Builder.
  • No Builder, forneça métodos que consomem self (por valor) para permitir encadeamento. Isso é ergonômico e evita clonagem desnecessária.
  • Considere validar os dados no método build() retornando Result em vez de sempre retornar um valor válido. Isso permite tratar erros de configuração.
  • Se o seu builder tiver muitos campos, considere usar o crate derive_builder para gerar a implementação automaticamente, mas entenda o que ele faz por baixo dos panos.
  • Use tipos fantasma se precisar garantir que campos obrigatórios sejam preenchidos em tempo de compilação, mas esteja ciente da complexidade adicional.

Exercícios

  1. Implemente o trait Default para um struct Ponto com campos x e y (f32) de forma que o padrão seja a origem (0.0, 0.0). Teste com um exemplo.
  2. ✓ Resposta:
    #[derive(Debug)]
    struct Ponto {
        x: f32,
        y: f32,
    }
    
    impl Default for Ponto {
        fn default() -> Self {
            Self { x: 0.0, y: 0.0 }
        }
    }
    
    fn main() {
        let p = Ponto::default();
        println!("{:?}", p); // Ponto { x: 0.0, y: 0.0 }
    }
  3. Crie um builder para o struct Pessoa com campos nome (String), idade (u8) e email (String). O builder deve permitir encadeamento e fornecer valores padrão para os campos não especificados.
  4. ✓ Resposta:
    #[derive(Debug)]
    struct Pessoa {
        nome: String,
        idade: u8,
        email: String,
    }
    
    struct PessoaBuilder {
        nome: String,
        idade: u8,
        email: String,
    }
    
    impl PessoaBuilder {
        fn new() -> Self {
            Self {
                nome: String::new(),
                idade: 0,
                email: String::new(),
            }
        }
    
        fn nome(mut self, nome: &str) -> Self {
            self.nome = nome.to_string();
            self
        }
    
        fn idade(mut self, idade: u8) -> Self {
            self.idade = idade;
            self
        }
    
        fn email(mut self, email: &str) -> Self {
            self.email = email.to_string();
            self
        }
    
        fn build(self) -> Pessoa {
            Pessoa {
                nome: self.nome,
                idade: self.idade,
                email: self.email,
            }
        }
    }
    
    fn main() {
        let p = PessoaBuilder::new()
            .nome("João")
            .idade(25)
            .build();
        println!("{:?}", p);
    }
  5. Modifique o builder do exercício anterior para usar Default no builder, definindo valores padrão como nome vazio, idade 0 e email vazio. Teste a criação sem especificar nenhum campo.
  6. ✓ Resposta:
    #[derive(Clone)]
    struct PessoaBuilder {
        nome: String,
        idade: u8,
        email: String,
    }
    
    impl Default for PessoaBuilder {
        fn default() -> Self {
            Self {
                nome: String::new(),
                idade: 0,
                email: String::new(),
            }
        }
    }
    
    impl PessoaBuilder {
        fn new() -> Self {
            Self::default()
        }
    
        fn nome(mut self, nome: &str) -> Self {
            self.nome = nome.to_string();
            self
        }
    
        fn idade(mut self, idade: u8) -> Self {
            self.idade = idade;
            self
        }
    
        fn email(mut self, email: &str) -> Self {
            self.email = email.to_string();
            self
        }
    
        fn build(self) -> Pessoa {
            Pessoa {
                nome: self.nome,
                idade: self.idade,
                email: self.email,
            }
        }
    }
    
    fn main() {
        let p = PessoaBuilder::new().build();
        println!("{:?}", p); // Pessoa { nome: "", idade: 0, email: "" }
    }
  7. No exemplo do servidor, adicione uma validação no método build() para garantir que a porta não seja 0. Se for, retorne um Result com uma mensagem de erro.
  8. ✓ Resposta:
    impl ServerConfigBuilder {
        // ... outros métodos
        fn build(self) -> Result<ServerConfig, String> {
            if self.config.port == 0 {
                Err("Porta deve ser diferente de zero".to_string())
            } else {
                Ok(self.config)
            }
        }
    }
    
    fn main() {
        let config = ServerConfigBuilder::new()
            .port(0)
            .build();
        match config {
            Ok(c) => println!("{:?}", c),
            Err(e) => eprintln!("Erro: {}", e),
        }
    }
  9. Pesquise sobre o crate derive_builder e escreva um exemplo usando #[derive(Builder)] para um struct simples. Mostre como o código gerado se compara com um builder manual.
  10. ✓ Resposta:
    use derive_builder::Builder;
    
    #[derive(Debug, Builder)]
    struct Ponto {
        x: f64,
        y: f64,
    }
    
    fn main() {
        let p = PontoBuilder::default()
            .x(1.0)
            .y(2.0)
            .build()
            .unwrap();
        println!("{:?}", p);
    }

    O crate gera um builder automaticamente, com métodos para cada campo e um método build() que retorna Result. Isso reduz muito o código manual, mas você perde controle sobre validações personalizadas, a menos que adicione atributos específicos.

Referências