Das Builder-Pattern ist die Standard-Antwort auf das Fehlen von Default-Parametern und überladenen Konstruktoren in Rust. Wenn ein Typ viele optionale Konfigurations-Werte hat, ist ein einziger new(...)-Konstruktor unhandlich — entweder zu viele Parameter, oder eine Lawine von new_with_a, new_with_a_and_b, new_with_everything. Der Builder löst das durch schrittweisen Aufbau mit Method-Chaining: erst Builder::neu(), dann konfigurieren mit .timeout(...).retries(...).header(...), am Ende .bauen(). Dieser Artikel zeigt die zwei Builder-Stile (konsumierend mit self vs. mutable mit &mut self), Fehlerbehandlung und das derive_builder-Crate.
Das Problem
Stell dir vor, du baust eine HTTP-Anfrage. Sie hat URL (Pflicht), Method (Default GET), Timeout (Default 5s), Headers (optional), Body (optional). Naive Lösung:
struct Anfrage { /* ... */ }
impl Anfrage {
// Variante 1: alle Parameter
fn new(url: String, method: String, timeout_ms: u32,
headers: Vec<(String, String)>, body: Vec<u8>) -> Self {
# Anfrage {}
}
// Variante 2: nur URL — Defaults für Rest
fn new_simple(url: String) -> Self { todo!() }
// Variante 3: URL + Method
fn new_with_method(url: String, method: String) -> Self { todo!() }
// ... etc.
}Schnell unübersichtlich. Lösung: Builder.
Konsumierender Builder (self-Style)
Der klassische Stil: jede Builder-Methode nimmt mut self, konfiguriert intern und gibt Self zurück.
pub struct AnfrageBuilder {
url: String,
method: String,
timeout_ms: u32,
headers: Vec<(String, String)>,
body: Vec<u8>,
}
impl AnfrageBuilder {
pub fn neu(url: impl Into<String>) -> Self {
AnfrageBuilder {
url: url.into(),
method: "GET".into(),
timeout_ms: 5000,
headers: Vec::new(),
body: Vec::new(),
}
}
pub fn method(mut self, m: &str) -> Self {
self.method = m.to_string();
self
}
pub fn timeout(mut self, ms: u32) -> Self {
self.timeout_ms = ms;
self
}
pub fn header(mut self, key: &str, wert: &str) -> Self {
self.headers.push((key.to_string(), wert.to_string()));
self
}
pub fn body(mut self, b: Vec<u8>) -> Self {
self.body = b;
self
}
pub fn bauen(self) -> Anfrage {
Anfrage {
url: self.url,
method: self.method,
timeout_ms: self.timeout_ms,
headers: self.headers,
body: self.body,
}
}
}
pub struct Anfrage {
pub url: String,
pub method: String,
pub timeout_ms: u32,
pub headers: Vec<(String, String)>,
pub body: Vec<u8>,
}
fn main() {
let anfrage = AnfrageBuilder::neu("https://example.com/api")
.method("POST")
.timeout(10_000)
.header("Accept", "application/json")
.header("X-Token", "abc123")
.body(b"{}".to_vec())
.bauen();
println!("{}: {}", anfrage.method, anfrage.url);
}Eigenschaften:
mut selfals Receiver — der Builder wird durch jede Methode gemoved.Self-Return — die Methode gibt sich selbst (modifiziert) zurück.bauen(self)konsumiert — wandelt den Builder in den finalen Typ um.- Aufrufer braucht keine
mut-Bindung — alles in einer Chain.
Mutable Builder (&mut-Style)
Alternative: jede Methode nimmt &mut self und gibt &mut Self zurück.
pub struct UrlBuilder {
schema: String,
host: String,
pfad: String,
params: Vec<(String, String)>,
}
impl UrlBuilder {
pub fn neu(host: impl Into<String>) -> Self {
UrlBuilder {
schema: "https".into(),
host: host.into(),
pfad: "/".into(),
params: Vec::new(),
}
}
pub fn pfad(&mut self, p: &str) -> &mut Self {
self.pfad = p.to_string();
self
}
pub fn param(&mut self, key: &str, wert: &str) -> &mut Self {
self.params.push((key.into(), wert.into()));
self
}
pub fn bauen(&self) -> String {
let query = if self.params.is_empty() {
String::new()
} else {
let p: Vec<String> = self.params.iter()
.map(|(k, v)| format!("{k}={v}"))
.collect();
format!("?{}", p.join("&"))
};
format!("{}://{}{}{}", self.schema, self.host, self.pfad, query)
}
}
fn main() {
let mut b = UrlBuilder::neu("api.example.com");
b.pfad("/users").param("page", "1").param("limit", "20");
let url = b.bauen();
// Builder ist hier noch nutzbar — kann weiter konfiguriert werden
assert!(url.contains("page=1"));
}Unterschiede:
&mut selfals Receiver — der Builder bleibt beim Aufrufer.&mut Self-Return für Chaining.- Aufrufer braucht
mut-Bindung —let mut b = .... bauen(&self)mit Borrow — der Builder existiert nach dem Bauen weiter.
Wann welcher Stil?
| Stil | Wann nutzen |
|---|---|
Konsumierend (self + Self) | Klassisch — Builder wird genau einmal verwendet |
Mutable (&mut self + &mut Self) | Wenn der Builder mehrfach genutzt oder zwischen Funktionen geteilt wird |
Konsumierend ist häufiger und prägnanter. Mutable ist nützlich, wenn der Builder „bestehen bleiben" soll oder seine Methoden aus verschiedenen Stellen aufgerufen werden.
Fallible Builder mit Result-Rückgabe
Wenn die Konfiguration fehlschlagen kann (z. B. ungültige URL, fehlende Pflicht-Felder), gibt bauen() ein Result zurück:
pub struct ServerBuilder {
host: Option<String>,
port: u16,
workers: u32,
}
impl ServerBuilder {
pub fn neu() -> Self {
ServerBuilder { host: None, port: 8080, workers: 4 }
}
pub fn host(mut self, h: impl Into<String>) -> Self {
self.host = Some(h.into());
self
}
pub fn port(mut self, p: u16) -> Self {
self.port = p;
self
}
pub fn bauen(self) -> Result<Server, &'static str> {
let host = self.host.ok_or("Host ist Pflicht")?;
if self.port < 1024 {
return Err("Port < 1024 ist privilegiert");
}
Ok(Server { host, port: self.port, workers: self.workers })
}
}
pub struct Server { host: String, port: u16, workers: u32 }
fn main() -> Result<(), &'static str> {
let s = ServerBuilder::neu()
.host("0.0.0.0")
.port(8080)
.bauen()?;
println!("Server auf {}:{} mit {} Workers", s.host, s.port, s.workers);
Ok(())
}Pflicht-Felder als Option<T> mit Default = None — bauen() prüft die Pflicht und validiert die Konfiguration.
Typestate-Builder mit Compile-Zeit-Pflicht-Felder
Eine elegantere Alternative: Pflicht-Felder werden über das Typ-System erzwungen.
use std::marker::PhantomData;
pub struct OhneHost;
pub struct MitHost;
pub struct ServerBuilder<S> {
host: Option<String>,
port: u16,
_state: PhantomData<S>,
}
impl ServerBuilder<OhneHost> {
pub fn neu() -> Self {
ServerBuilder { host: None, port: 8080, _state: PhantomData }
}
pub fn host(self, h: impl Into<String>) -> ServerBuilder<MitHost> {
ServerBuilder {
host: Some(h.into()),
port: self.port,
_state: PhantomData,
}
}
}
impl<S> ServerBuilder<S> {
pub fn port(mut self, p: u16) -> Self {
self.port = p;
self
}
}
impl ServerBuilder<MitHost> {
pub fn bauen(self) -> Server {
Server {
host: self.host.unwrap(), // sicher — MitHost garantiert Some
port: self.port,
}
}
}
pub struct Server { host: String, port: u16 }
fn main() {
// Korrekt:
let _ = ServerBuilder::neu()
.host("0.0.0.0")
.port(8080)
.bauen();
// Compile-Fehler — bauen ohne host:
// let _ = ServerBuilder::neu().port(8080).bauen();
}bauen() ist nur am ServerBuilder<MitHost> verfügbar. Wer host vergisst, bekommt einen Compile-Fehler, kein Runtime-Error. Klassisches Typestate-Pattern — mehr Aufwand, dafür typsicher.
derive_builder-Crate
Für viele Felder lohnt sich das derive_builder-Crate, das den Builder automatisch generiert:
# // Erfordert Cargo.toml: derive_builder = "0.20"
use derive_builder::Builder;
#[derive(Builder, Debug)]
#[builder(setter(into))]
pub struct Anfrage {
url: String,
#[builder(default = "\"GET\".into()")]
method: String,
#[builder(default = "5000")]
timeout_ms: u32,
}
fn main() -> Result<(), AnfrageBuilderError> {
let a = AnfrageBuilder::default()
.url("https://example.com")
.method("POST")
.build()?;
println!("{a:?}");
Ok(())
}Das Macro generiert AnfrageBuilder-Struct, default()-Konstruktor, Setter pro Feld und build()-Methode. Spart erheblichen Boilerplate-Code bei großen Konfigurations-Strukturen.
Praxis: Builder im echten Code
HTTP-Client
pub struct HttpClientBuilder {
base_url: Option<String>,
timeout_ms: u32,
user_agent: String,
tls_verify: bool,
}
impl HttpClientBuilder {
pub fn neu() -> Self {
HttpClientBuilder {
base_url: None,
timeout_ms: 30_000,
user_agent: "MeineApp/1.0".into(),
tls_verify: true,
}
}
pub fn base_url(mut self, url: impl Into<String>) -> Self {
self.base_url = Some(url.into());
self
}
pub fn timeout_sec(mut self, s: u32) -> Self {
self.timeout_ms = s * 1000;
self
}
pub fn user_agent(mut self, ua: impl Into<String>) -> Self {
self.user_agent = ua.into();
self
}
pub fn tls_unsicher(mut self) -> Self {
self.tls_verify = false;
self
}
pub fn bauen(self) -> Result<HttpClient, &'static str> {
Ok(HttpClient {
base_url: self.base_url.ok_or("base_url fehlt")?,
timeout_ms: self.timeout_ms,
user_agent: self.user_agent,
tls_verify: self.tls_verify,
})
}
}
pub struct HttpClient {
pub base_url: String,
pub timeout_ms: u32,
pub user_agent: String,
pub tls_verify: bool,
}
fn main() -> Result<(), &'static str> {
let client = HttpClientBuilder::neu()
.base_url("https://api.example.com")
.timeout_sec(15)
.user_agent("MeineApp/2.0")
.bauen()?;
println!("{} ({}s timeout)", client.base_url, client.timeout_ms / 1000);
Ok(())
}Klassischer Builder für eine Library-Komponente. Pflicht-Feld via Option + ?-Operator beim Bauen.
Database-Connection-Builder
pub struct ConnectionBuilder {
host: String,
port: u16,
database: String,
username: String,
password: Option<String>,
pool_size: u32,
ssl: bool,
}
impl ConnectionBuilder {
pub fn an(host: impl Into<String>, database: impl Into<String>) -> Self {
ConnectionBuilder {
host: host.into(),
port: 5432,
database: database.into(),
username: "postgres".into(),
password: None,
pool_size: 10,
ssl: true,
}
}
pub fn port(mut self, p: u16) -> Self { self.port = p; self }
pub fn login(mut self, user: &str, pw: &str) -> Self {
self.username = user.into();
self.password = Some(pw.into());
self
}
pub fn pool(mut self, size: u32) -> Self { self.pool_size = size; self }
pub fn ohne_ssl(mut self) -> Self { self.ssl = false; self }
pub fn verbinden(self) -> Result<Connection, String> {
println!("Verbinde zu {}:{}/{}", self.host, self.port, self.database);
Ok(Connection { /* ... */ })
}
}
pub struct Connection;an(...) als sprechender Konstruktor mit zwei Pflicht-Argumenten, dann fluent-konfigurieren. verbinden() als finale Aktion, die ein Result liefert.
Email-Builder
pub struct EmailBuilder {
von: Option<String>,
an: Vec<String>,
cc: Vec<String>,
betreff: String,
inhalt: String,
anhaenge: Vec<String>,
}
impl EmailBuilder {
pub fn neu() -> Self {
EmailBuilder {
von: None, an: Vec::new(), cc: Vec::new(),
betreff: String::new(), inhalt: String::new(),
anhaenge: Vec::new(),
}
}
pub fn von(mut self, addr: &str) -> Self {
self.von = Some(addr.into()); self
}
pub fn an(mut self, addr: &str) -> Self {
self.an.push(addr.into()); self
}
pub fn cc(mut self, addr: &str) -> Self {
self.cc.push(addr.into()); self
}
pub fn betreff(mut self, s: &str) -> Self {
self.betreff = s.into(); self
}
pub fn inhalt(mut self, s: &str) -> Self {
self.inhalt = s.into(); self
}
pub fn anhang(mut self, pfad: &str) -> Self {
self.anhaenge.push(pfad.into()); self
}
pub fn senden(self) -> Result<(), &'static str> {
if self.von.is_none() { return Err("von fehlt"); }
if self.an.is_empty() { return Err("keine Empfänger"); }
println!("Sende: {}", self.betreff);
Ok(())
}
}
fn main() -> Result<(), &'static str> {
EmailBuilder::neu()
.von("admin@example.com")
.an("user@example.com")
.cc("backup@example.com")
.betreff("Hallo")
.inhalt("Test")
.anhang("/tmp/foo.pdf")
.senden()?;
Ok(())
}Listen-Felder (an, cc, anhaenge) — der Builder akkumuliert Werte über mehrere Aufrufe.
Query-Builder
pub struct QueryBuilder {
tabelle: String,
where_klauseln: Vec<String>,
order_by: Option<String>,
limit: Option<u32>,
}
impl QueryBuilder {
pub fn select_aus(tabelle: &str) -> Self {
QueryBuilder {
tabelle: tabelle.into(),
where_klauseln: Vec::new(),
order_by: None,
limit: None,
}
}
pub fn wo(mut self, klausel: &str) -> Self {
self.where_klauseln.push(klausel.into()); self
}
pub fn sortieren_nach(mut self, feld: &str) -> Self {
self.order_by = Some(feld.into()); self
}
pub fn maximal(mut self, n: u32) -> Self {
self.limit = Some(n); self
}
pub fn bauen(self) -> String {
let mut q = format!("SELECT * FROM {}", self.tabelle);
if !self.where_klauseln.is_empty() {
q.push_str(&format!(" WHERE {}", self.where_klauseln.join(" AND ")));
}
if let Some(o) = self.order_by {
q.push_str(&format!(" ORDER BY {o}"));
}
if let Some(l) = self.limit {
q.push_str(&format!(" LIMIT {l}"));
}
q
}
}
fn main() {
let q = QueryBuilder::select_aus("users")
.wo("aktiv = true")
.wo("alter > 18")
.sortieren_nach("erstellt_am DESC")
.maximal(50)
.bauen();
println!("{q}");
}Klassisches Query-Builder-Pattern — alle Database-ORMs in Rust nutzen dieses Schema.
Logger-Konfigurations-Builder
pub struct LoggerBuilder {
level: String,
output: String,
format_json: bool,
farbe: bool,
}
impl Default for LoggerBuilder {
fn default() -> Self {
LoggerBuilder {
level: "INFO".into(),
output: "stderr".into(),
format_json: false,
farbe: true,
}
}
}
impl LoggerBuilder {
pub fn level(mut self, l: &str) -> Self { self.level = l.into(); self }
pub fn nach_datei(mut self, pfad: &str) -> Self {
self.output = pfad.into(); self
}
pub fn json(mut self) -> Self { self.format_json = true; self }
pub fn ohne_farbe(mut self) -> Self { self.farbe = false; self }
pub fn bauen(self) -> Logger {
Logger { /* ... */ }
}
}
pub struct Logger;
fn main() {
let _logger = LoggerBuilder::default()
.level("DEBUG")
.nach_datei("/var/log/app.log")
.json()
.bauen();
}Default-Trait für sinnvolle Initial-Konfiguration. Builder-Methoden überschreiben gezielt.
Test-Daten-Builder
pub struct UserBuilder {
id: u64,
name: String,
email: String,
ist_admin: bool,
}
impl UserBuilder {
pub fn test_user() -> Self {
UserBuilder {
id: 1,
name: "Test User".into(),
email: "test@example.com".into(),
ist_admin: false,
}
}
pub fn name(mut self, n: &str) -> Self { self.name = n.into(); self }
pub fn admin(mut self) -> Self { self.ist_admin = true; self }
pub fn bauen(self) -> User {
User { id: self.id, name: self.name, email: self.email, ist_admin: self.ist_admin }
}
}
pub struct User { pub id: u64, pub name: String, pub email: String, pub ist_admin: bool }
#[test]
fn test_beispiel() {
let admin = UserBuilder::test_user().admin().bauen();
let user = UserBuilder::test_user().name("Spezial").bauen();
assert!(admin.ist_admin);
assert_eq!(user.name, "Spezial");
}Test-Builder mit sinnvollen Defaults. Pro Test wird gezielt überschrieben, was abweicht.
Interessantes
Konsumierender Builder ist idiomatischer.
Der self + Self-Stil ist häufiger und kompakter. Aufrufer braucht keine mut-Bindung, Chain-Syntax ist sauberer. &mut self + &mut Self ist nützlich, wenn der Builder mehrfach verwendet wird oder zwischen Funktionen geteilt.
bauen() als finale Methode mit self.
Die abschließende Methode (bauen, build, finish, compile) nimmt typischerweise self und verbraucht den Builder. Sie liefert den finalen Typ — meist Self als anderer Typ, oder Result<Self, Error> bei Fehler-Möglichkeit.
Pflicht-Felder mit Option oder Typestate.
Zwei Patterns für Pflicht-Felder. (1) Option<T> plus Check in bauen() — Runtime-Error bei fehlendem Wert. (2) Typestate mit Phantom-States — Compile-Error bei fehlendem Wert. Typestate ist sicherer, aber aufwendiger.
impl Into für flexible Parameter.
Builder-Setter sollten impl Into<String> oder impl Into<...> akzeptieren — der Aufrufer kann &str, String, Cow<str> übergeben, ohne dass der Builder mehrere Methoden bietet. Standard-Pattern in idiomatischem Rust.
derive_builder-Crate automatisiert die Boilerplate.
Für Builder mit 5+ Feldern lohnt sich das derive_builder-Crate. #[derive(Builder)] generiert den XYZBuilder-Struct mit Setter und build()-Methode automatisch. Konfiguration über #[builder(...)]-Attribute pro Feld.
Listen-Felder akkumulieren über mehrere Setter-Calls.
EmailBuilder::neu().an("a@x").an("b@y") fügt beide zur an-Liste hinzu. Standard-Pattern für Felder, die mehrfach gesetzt werden sollen.
Builder-Methoden sind Self-dokumentierend.
Anfrage::neu(...)::method(...)::timeout(...)::bauen() liest sich fast wie ein Satz. Ein Konsument der API sieht beim Aufruf, was er konfiguriert — viel klarer als positionale Argumente in einem new(...).
Default-Trait für initiale Werte.
Wer einen Builder hat, dessen Default-Werte alle Stdlib-Defaults sind: #[derive(Default)] generiert die default()-Methode. Statt Builder::neu() heißt's dann Builder::default() — beides idiomatisch.
Weiterführende Ressourcen
Externe Quellen
- Rust Design Patterns – Builder
- derive_builder Crate
- Rust API Guidelines – Builders
- The Rust Book – Method Syntax