Einleitung: Warum ist Dependency Injection in Rust wichtig?
\n\nBeim Schreiben von Programmen in Rust werden Sie früher oder später auf die Situation stoßen, dass ein Modul (z. B. ein Service oder Controller) die Fähigkeiten eines anderen Moduls (Datenbank, API, Logger) nutzen muss. Der einfachste Weg ist, das benötigte Objekt direkt innerhalb des verwendenden Codes zu erstellen:
\n\nstruct UserService;\n\nimpl UserService {\n fn get_user(&self, id: u32) -> String {\n let db = Database::new(); // ❌ Feste Kopplung\n db.query_user(id)\n }\n}\n\n\nDieser Ansatz erzeugt eine feste Kopplung (tight coupling). Der Code wird schwer zu testen, wiederzuverwenden und zu ändern. Dependency Injection (DI) ist ein Entwurfsmuster, bei dem Abhängigkeiten von außen an ein Objekt übergeben werden, anstatt innerhalb des Objekts erstellt zu werden. Rust, als Sprache mit einem Besitzsystem und Traits, bietet elegante Möglichkeiten, DI ohne Frameworks zu implementieren.
\n\nIn diesem Artikel werden wir anhand praktischer Beispiele untersuchen, wie man Abhängigkeiten in Rust injiziert: von der einfachen Übergabe von Strukturen bis hin zur Verwendung von Traits und intelligenten Zeigern.
\n\n1. Grundlegender Ansatz: Abhängigkeiten über den Konstruktor übergeben
\n\nDer einfachste Weg, eine Abhängigkeit in Rust zu injizieren, besteht darin, sie als Argument eines Konstruktors oder einer Funktion zu übergeben. Betrachten wir ein Beispiel mit einem Logger.
\n\nSchlechtes Beispiel (ohne DI)
\n\nstruct ConsoleLogger;\n\nimpl ConsoleLogger {\n fn log(&self, msg: &str) {\n println!("[LOG]: {}", msg);\n }\n}\n\nstruct OrderService {\n logger: ConsoleLogger, // ❌ Feste Bindung an einen bestimmten Typ\n}\n\nimpl OrderService {\n fn new() -> Self {\n Self {\n logger: ConsoleLogger, // Wird intern erstellt\n }\n }\n\n fn place_order(&self, item: &str) {\n self.logger.log(&format!("Bestellung: {}", item));\n }\n}\n\n\nGutes Beispiel (mit DI)
\n\nstruct ConsoleLogger;\n\nimpl ConsoleLogger {\n fn log(&self, msg: &str) {\n println!("[LOG]: {}", msg);\n }\n}\n\nstruct OrderService {\n logger: ConsoleLogger, // Abhängigkeit wird von außen übergeben\n}\n\nimpl OrderService {\n // Konstruktor nimmt die Abhängigkeit entgegen\n fn new(logger: ConsoleLogger) -> Self {\n Self { logger }\n }\n\n fn place_order(&self, item: &str) {\n self.logger.log(&format!("Bestellung: {}", item));\n }\n}\n\nfn main() {\n let logger = ConsoleLogger;\n let service = OrderService::new(logger);\n service.place_order("Rust-Buch");\n}\n\n\nWas hat sich geändert? Jetzt erstellt OrderService den Logger nicht selbst. Er erhält ihn fertig. Das ist bereits DI, aber es gibt eine Einschränkung: Wir sind immer noch an den konkreten Typ ConsoleLogger gebunden. Um den Code flexibel zu machen, müssen wir Traits verwenden.
2. Verwendung von Traits zur Abstraktion von Abhängigkeiten
\n\nTraits in Rust sind das Äquivalent zu Interfaces in anderen Sprachen. Sie ermöglichen es, einen Vertrag zu definieren, dem die Abhängigkeiten folgen müssen.
\n\nDefinieren des Logger-Traits
\n\ntrait Logger {\n fn log(&self, msg: &str);\n}\n\nstruct ConsoleLogger;\nimpl Logger for ConsoleLogger {\n fn log(&self, msg: &str) {\n println!("[Konsole]: {}", msg);\n }\n}\n\nstruct FileLogger {\n filename: String,\n}\nimpl Logger for FileLogger {\n fn log(&self, msg: &str) {\n // Simulation des Schreibens in eine Datei\n println!("[Datei {}]: {}", self.filename, msg);\n }\n}\n\n\nInjizieren des Traits über Generics
\n\nstruct OrderService<L: Logger> {\n logger: L, // Generischer Typ, der das Logger-Trait implementiert\n}\n\nimpl<L: Logger> OrderService<L> {\n fn new(logger: L) -> Self {\n Self { logger }\n }\n\n fn place_order(&self, item: &str) {\n self.logger.log(&format!("Bestellung: {}", item));\n }\n}\n\nfn main() {\n let console_logge