Einleitung: SOLID-Prinzipien in Rust — Warum ist das nötig?
SOLID ist eine Sammlung von fünf grundlegenden Prinzipien der objektorientierten Programmierung, die helfen, flexiblen, wartbaren und skalierbaren Code zu erstellen. Obwohl Rust keine klassische OOP-Sprache ist (es hat keine Klassenvererbung im üblichen Sinne), ermöglichen sein Ownership-System, Traits und Generics eine hervorragende Umsetzung aller SOLID-Prinzipien. Das Verständnis dieser Prinzipien im Kontext von Rust hilft, Code zu schreiben, der leicht zu testen, umzugestalten und zu erweitern ist.
In diesem Artikel analysieren wir jedes der fünf SOLID-Prinzipien mit praktischen Beispielen in Rust. Sie werden sehen, wie trait Interfaces ersetzt, wie Box<dyn Trait> bei der Abhängigkeitsinversion hilft und wie man häufige Fehler vermeidet.
1. Single-Responsibility-Prinzip (SRP) in Rust
Single-Responsibility-Prinzip (SRP): Jedes Modul, Struct oder jede Funktion sollte nur einen Grund für eine Änderung haben. In Rust bedeutet dies, dass ein Struct für eine einzelne Entität verantwortlich sein sollte, anstatt Geschäftslogik, Datenverarbeitung und Ausgabe zu vermischen.
Beispiel einer SRP-Verletzung
struct User { name: String, email: String,}
impl User { // Vermischung von Geschäftslogik und Persistenz fn save_to_db(&self) { println!("Speichere {} in der DB", self.name); } fn send_email(&self, message: &str) { println!("Sende E-Mail an {}: {}", self.email, message); }}Hier ist das User-Struct verantwortlich für Benutzerdaten, Persistenz und das Senden von E-Mails. Dies verstößt gegen das SRP.
Korrekte SRP-Implementierung
struct User { name: String, email: String,}
struct UserRepository;impl UserRepository { fn save(&self, user: &User) { println!("Speichere {} in der DB", user.name); }}
struct EmailService;impl EmailService { fn send(&self, user: &User, message: &str) { println!("Sende E-Mail an {}: {}", user.email, message); }}Jetzt hat jedes Struct eine einzige Verantwortung: User — Daten, UserRepository — Persistenz, EmailService — Versand. Der Code ist modularer und testbarer geworden.
2. Open/Closed-Prinzip (OCP) mit Traits
Open/Closed-Prinzip (OCP): Software-Entitäten sollten offen für Erweiterung, aber geschlossen für Modifikation sein. In Rust wird dies idealerweise mit trait und dynamischem Dispatch umgesetzt.
Beispiel ohne OCP
fn calculate_area(shape: &str, params: &[f64]) -> f64 { match shape { "circle" => std::f64::consts::PI * params[0] * params[0], "rectangle" => params[0] * params[1], _ => panic!("Unbekannte Form"), }}Beim Hinzufügen einer neuen Form (z. B. Dreieck) muss die Funktion modifiziert werden.
Korrekter Ansatz mit OCP
trait Shape { fn area(&self) -> f64;}
struct Circle { radius: f64 }impl Shape for Circle { fn area(&self) -> f64 { std::f64::consts::PI * self.radius * self.radius }}
struct Rectangle { width: f64, height: f64 }impl Shape for Rectangle { fn area(&self) -> f64 { self.width * self.height }}
// Funktion ist geschlossen für Modifikation, aber offen für Erweiterungfn print_area(shape: &dyn Shape) { println!("Fläche: {}", shape.area());}Um ein Dreieck hinzuzufügen, muss man jetzt nur ein neues Struct erstellen und das trait Shape implementieren. Die print_area-Funktion benötigt keine Änderungen.
3. Liskovsches Substitutionsprinzip (LSP) und Vererbung mit Traits
Liskovsches Substitutionsprinzip (LSP): Objekte in einem Programm sollten durch Instanzen von