Einleitung: SOLID in der Welt von Rust
\n\nDie SOLID-Prinzipien sind das Fundament einer flexiblen und wartbaren Architektur. Ihre Implementierung in Rust hat jedoch ihre eigene Spezifik, die durch das einzigartige Ownership-System, Traits und das Fehlen klassischer OOP bedingt ist. In diesem Artikel werden wir analysieren, wie jedes der fünf Prinzipien auf fortgeschrittenem Niveau unter Verwendung idiomatischen Rusts angewendet wird. Wir werden nicht nur die Regeln auflisten, sondern zeigen, wie sie im Kontext des Typsystems und des Borrowings transformiert werden.
\n\nDieser Artikel richtet sich an Entwickler, die bereits mit den Grundlagen von Rust und SOLID vertraut sind. Wir konzentrieren uns auf die Feinheiten: wie man eine Verletzung des LSP bei der Arbeit mit generischen Typen vermeidet, wie ISP das Design von Traits beeinflusst und warum DIP in Rust oft die Verwendung von Trait-Objekten oder Box erfordert.
1. Single-Responsibility-Prinzip (SRP) und Ownership
\n\nIn Rust erhält SRP aufgrund der strengen Ownership-Regeln eine zusätzliche Dimension. Eine Klasse (oder Struktur) sollte nur einen Grund für eine Änderung haben. Aber wie verhält sich dies zur Übergabe von Ownership?
\n\nProblem: Struktur, die Ressourcen und Logik verwaltet
\nBetrachten wir ein Beispiel, bei dem eine Struktur gleichzeitig einen Dateideskriptor verwaltet und Daten parst. Dies verletzt SRP.
\n\nuse std::fs::File;\nuse std::io::Read;\n\n// ❌ SRP-Verletzung: Die Struktur verwaltet sowohl die Ressource als auch die Datenverarbeitung\nstruct DataProcessor {\n file: File,\n}\n\nimpl DataProcessor {\n fn new(path: &str) -> Self {\n DataProcessor {\n file: File::open(path).unwrap(),\n }\n }\n\n fn process(&mut self) -> Vec<u8> {\n let mut buffer = Vec::new();\n self.file.read_to_end(&mut buffer).unwrap();\n // ... komplexe Verarbeitungslogik\n buffer\n }\n}\n\n\nLösung: Trennung durch Ownership-Übergabe
\nWir teilen die Verantwortlichkeiten auf: Eine Struktur ist für das Lesen zuständig, eine andere für die Verarbeitung. Das Ownership wird explizit übergeben.
\n\nuse std::fs::File;\nuse std::io::Read;\n\n// ✅ Erste Verantwortung: Dateiverwaltung\nstruct FileReader {\n file: File,\n}\n\nimpl FileReader {\n fn new(path: &str) -> Self {\n FileReader {\n file: File::open(path).unwrap(),\n }\n }\n\n fn read(self) -> Vec<u8> {\n let mut buffer = Vec::new();\n // self.file wird konsumiert (moved), da sie nach dem Lesen nicht mehr benötigt wird\n self.file.read_to_end(&mut buffer).unwrap();\n buffer\n }\n}\n\n// ✅ Zweite Verantwortung: Datenverarbeitung\nstruct DataProcessor {\n data: Vec<u8>,\n}\n\nimpl DataProcessor {\n fn new(data: Vec<u8>) -> Self {\n DataProcessor { data }\n }\n\n fn process(&self) -> Vec<u8> {\n // Unveränderliche Verarbeitung\n self.data.iter().map(|&b| b + 1).collect()\n }\n}\n\nfn main() {\n let reader = FileReader::new("data.bin");\n let raw_data = reader.read(); // reader ist nicht mehr verfügbar\n let processor = DataProcessor::new(raw_data);\n let result = processor.process();\n}\n\n\nDer entscheidende Punkt: Wir haben self (Ownership) anstelle von &self in read() verwendet, um explizit anzuzeigen, dass FileReader nach dem Lesen nicht mehr benötigt wird. Dies macht den Datenfluss transparent und verhindert Ressourcenlecks.
2. Open/Closed-Prinzip (OCP) und Traits
\n\nOCP besagt: Entitäten sollten offen für Erweiterung, aber geschlossen für Modifikation sein. In Rust wird dies ideal durch Traits und Generics ohne Verwendung von virtuellen Tabellen (vtable) umgesetzt, sofern dies nicht erforderlich ist.
\n\nFortgeschrittenes Beispiel: Strategie mit null Overhead
\nAngenommen, wir haben ein Logging-System. Anstatt den Kern der Anwendung für jedes neue Backend zu modifizieren, verwenden wir Polymorphismus durch Traits.
\n\n/