Einleitung: Warum SOLID in Rust wichtig ist?
SOLID – das sind fünf grundlegende Prinzipien des objektorientierten Designs, die helfen, flexiblen, wartbaren und skalierbaren Code zu erstellen. Obwohl Rust keine klassische OOP-Sprache ist (es gibt keine Klassenvererbung im herkömmlichen Sinne), sind diese Prinzipien auch hier dank Traits, Strukturen, Generics und Speicherverwaltung anwendbar. In diesem Artikel analysieren wir jedes SOLID-Prinzip anhand von Codebeispielen in Rust, zeigen, wie man sie umsetzt, und erklären, warum sie Ihren Code verbessern.
1. Single Responsibility Principle (SRP): Prinzip der einzigen Verantwortung
Kernaussage: Jedes Modul, jede Struktur oder Funktion sollte nur einen Grund für eine Änderung haben.
In Rust wird dies oft verletzt, wenn eine Struktur sowohl für die Datenspeicherung als auch für deren Serialisierung und die Geschäftslogik verantwortlich ist. Der richtige Ansatz ist, die Verantwortlichkeiten zu trennen.
Schlechtes Beispiel (Verletzung des SRP)
struct User { name: String, email: String,}
impl User { fn save_to_db(&self) { // Logik zum Speichern in der Datenbank }
fn send_email(&self, message: &str) { // Logik zum Senden einer E-Mail }}Hier ist User sowohl für die Speicherung als auch für das Senden verantwortlich – zwei Gründe für eine Änderung.
Gutes Beispiel (Einhaltung des SRP)
struct User { name: String, email: String,}
struct UserRepository;impl UserRepository { fn save(&self, user: &User) { /* ... */ }}
struct EmailService;impl EmailService { fn send(&self, user: &User, message: &str) { /* ... */ }}Jetzt hat jede Struktur eine einzige Verantwortung: User – Daten, UserRepository – Speicherung, EmailService – Versand.
2. Open-Closed Principle (OCP): Prinzip der Offenheit/Geschlossenheit
Kernaussage: Softwareentitäten sollten für Erweiterungen offen, aber für Modifikationen geschlossen sein.
In Rust wird dies durch Traits und Generics erreicht. Sie können neues Verhalten hinzufügen, ohne vorhandenen Code zu ändern.
Beispiel: Rabattsystem
Angenommen, wir haben eine Funktion zur Berechnung des Preises mit Rabatt. Anstatt neue Rabattarten über if-else hinzuzufügen, verwenden wir ein Trait:
trait Discount { fn apply(&self, price: f64) -> f64;}
struct NoDiscount;impl Discount for NoDiscount { fn apply(&self, price: f64) -> f64 { price }}
struct SeasonalDiscount(f64);impl Discount for SeasonalDiscount { fn apply(&self, price: f64) -> f64 { price * (1.0 - self.0) }}
fn calculate_price(price: f64, discount: &dyn Discount) -> f64 { discount.apply(price)}Um nun einen neuen Rabatt hinzuzufügen (z. B. BlackFridayDiscount), müssen wir calculate_price nicht ändern – wir implementieren einfach das Trait Discount.
3. Liskov Substitution Principle (LSP): Prinzip der Substitution von Barbara Liskov
Kernaussage: Objekte von Subtypen (in Rust – Typen, die ein Trait implementieren) sollten austauschbar mit Objekten des Basistyps sein, ohne die Korrektheit des Programms zu beeinträchtigen.
In Rust bedeutet dies, dass Code, der mit dyn Trait oder Generics arbeitet, nicht brechen sollte, wenn eine beliebige Implementierung eingesetzt wird.
Beispiel für eine LSP-Verletzung
trait Bird { fn fly(&self) -> String;}
struct Sparrow;impl Bird for Sparrow { fn fly(&self) -> String { "Sparrow flies".to_string() }}
struct Penguin;impl Bird for Penguin { fn fly(&self) -> String { panic!("Penguins can't fly!") }}Wenn wir Sparrow durch Penguin ersetzen, bricht der Code. Die Lösung: Fügen Sie keine Methoden hinzu, die nicht von allen Subtypen implementiert werden können. Besser ist es, die Traits zu trennen:
trait CanFly { fn fly(&self) -> Stri