Entwurfsmuster in Rust: praktischer Leitfaden mit Beispielen
Entwurfsmuster sind bewährte Lösungen für typische Entwicklungsaufgaben. In Rust unterscheidet sich die Implementierung klassischer Muster aufgrund des einzigartigen Besitzsystems und Speichermodells oft von Sprachen mit Garbage Collection. In diesem Artikel werden wir drei Schlüsselmuster behandeln: Singleton, Builder und Strategy, angepasst an die Besonderheiten von Rust.
1. Singleton-Muster in Rust: globaler Zustand ohne unsafe
Singleton stellt sicher, dass eine Klasse nur eine Instanz hat, und bietet einen globalen Zugriffspunkt darauf. In Rust eignet sich dafür ideal die Crate once_cell oder das stabile std::sync::OnceLock (seit Rust 1.70).
Implementierung mit OnceLock
use std::sync::OnceLock;
pub struct Logger { level: String,}
impl Logger { pub fn global() -> &'static Logger { static INSTANCE: OnceLock<Logger> = OnceLock::new(); INSTANCE.get_or_init(|| Logger { level: "info".to_string(), }) }
pub fn log(&self, msg: &str) { println!("[{}] {}", self.level, msg); }}
fn main() { let logger = Logger::global(); logger.log("Hallo, Rust!"); // Logger::global() gibt dieselbe Instanz zurück}Vorteile: Sicherheit (kein unsafe), faule Initialisierung, Threadsicherheit. Nachteil: globaler Zustand erschwert das Testen.
2. Builder-Muster: Konstruktion komplexer Objekte
Builder trennt die Konstruktion eines komplexen Objekts von seiner Darstellung. In Rust ist es besonders nützlich für Strukturen mit vielen optionalen Feldern.
Beispiel: Konfiguration eines HTTP-Clients
pub struct HttpClient { url: String, timeout: u64, headers: Vec<(String, String)>,}
pub struct HttpClientBuilder { url: String, timeout: Option<u64>, headers: Vec<(String, String)>,}
impl HttpClientBuilder { pub fn new(url: &str) -> Self { HttpClientBuilder { url: url.to_string(), timeout: None, headers: vec![], } }
pub fn timeout(mut self, secs: u64) -> Self { self.timeout = Some(secs); self }
pub fn header(mut self, key: &str, value: &str) -> Self { self.headers.push((key.to_string(), value.to_string())); self }
pub fn build(self) -> HttpClient { HttpClient { url: self.url, timeout: self.timeout.unwrap_or(30), headers: self.headers, } }}
fn main() { let client = HttpClientBuilder::new("https://api.example.com") .timeout(10) .header("Authorization", "Bearer token123") .build(); println!("Client-URL: {}", client.url);}Wichtig: Die Verwendung von self (nicht &self) in den Methoden des Builders vermeidet Klonen und macht die API ergonomischer.
3. Strategy-Muster: Algorithmen als austauschbare Strategien
Strategy definiert eine Familie von Algorithmen, kapselt jeden einzelnen und macht sie austauschbar. In Rust wird dies durch Traits und dynamische Dispatch (Box<dyn Trait>) implementiert.
Beispiel: Sortiersystem
trait SortStrategy { fn sort(&self, data: &mut [i32]);}
struct BubbleSort;impl SortStrategy for BubbleSort { fn sort(&self, data: &mut [i32]) { let n = data.len(); for i in 0..n { for j in 0..n - i - 1 { if data[j] > data[j + 1] { data.swap(j, j + 1); } } } }}
struct QuickSort;impl SortStrategy for QuickSort { fn sort(&self, da
```