Entwurfsmuster in Rust mit Beispielen
Entwurfsmuster sind bewährte Lösungen für wiederkehrende Aufgaben in der Softwareentwicklung. In Rust erfordert die Implementierung klassischer GoF-Muster aufgrund des einzigartigen Systems von Ownership, Borrowing und Lifetimes oft einen besonderen Ansatz. In diesem Artikel analysieren wir mehrere beliebte Muster anhand von echtem Rust-Code.
Warum verändert Rust den Umgang mit Mustern?
Im Gegensatz zu Sprachen mit Garbage Collection (Java, C#) legt Rust strenge Regeln für die Speicherverwaltung fest. Dies macht einige klassische Implementierungen (z.B. Singleton durch globalen veränderlichen Zustand) unsicher oder ineffizient. Allerdings bietet Rust eigene elegante Alternativen, die auf Typen und Traits basieren.
1. Builder – Schrittweise Objekterstellung
Builder ist eines der am häufigsten verwendeten Muster in Rust. Es eignet sich ideal für die Konfiguration komplexer Strukturen, besonders wenn viele optionale Felder vorhanden sind.
Beispiel: Erstellung einer HTTP-Anfrage
struct Request { method: String, url: String, headers: Vec<(String, String)>, body: Option<String>,}
struct RequestBuilder { method: String, url: String, headers: Vec<(String, String)>, body: Option<String>,}
impl RequestBuilder { fn new(method: &str, url: &str) -> Self { Self { method: method.to_string(), url: url.to_string(), headers: Vec::new(), body: None, } }
fn header(mut self, key: &str, value: &str) -> Self { self.headers.push((key.to_string(), value.to_string())); self }
fn body(mut self, body: &str) -> Self { self.body = Some(body.to_string()); self }
fn build(self) -> Request { Request { method: self.method, url: self.url, headers: self.headers, body: self.body, } }}
fn main() { let req = RequestBuilder::new("POST", "https://api.example.com/data") .header("Content-Type", "application/json") .header("Authorization", "Bearer token123") .body(r#"{"key": "value"}"#) .build();
println!("URL: {}", req.url); println!("Headers count: {}", req.headers.len());}Vorteile des Ansatzes in Rust
- Sicherheit: Das Objekt wird erst nach dem Aufruf von
build()erstellt – eine unvollständige Konfiguration ist unmöglich. - Lesbarkeit: Die Methodenkette ist intuitiv verständlich.
- Typsicherheit: Der Compiler prüft die Typen bei jedem Schritt.
2. Strategy – Algorithmusauswahl zur Laufzeit
Strategy ermöglicht es, das Verhalten eines Objekts zur Laufzeit zu ändern. In Rust wird dies durch Traits und dynamische Dispatch (Box mit Traits) oder statische Dispatch (Generics) implementiert.
Beispiel: Datensortierung
trait SortStrategy { fn sort(&self, data: &mut [i32]);}
struct BubbleSort;struct QuickSort;
impl SortStrategy for BubbleSort { fn sort(&self, data: &mut [i32]) { // Einfachste Implementierung von Bubble Sort for i in 0..data.len() { for j in 0..data.len() - 1 - i { if data[j] > data[j + 1] { data.swap(j, j + 1); } } } }}
impl SortStrategy for QuickSort { fn sort(&self, data: &mut [i32]) { // Quick Sort (vereinfacht) if data.len() > 1 { let pivot = data.len() / 2; data.sort(); // Nutzt die eingebaute Sortierung als Beispiel } }}
struct Sorter { strategy: Box<dyn SortStrategy>,}
impl Sorter { fn new(strategy: Box<dyn SortStrategy>) ->