Dependency Injection in Rust: in der Praxis

Online Python-Trainer für Anfänger

Lernen Sie Python einfach ohne theoretische Überlastung. Lösen Sie praktische Aufgaben mit automatischer Überprüfung und schreiben Sie Code direkt im Browser.

Kurs starten

Dependency Injection in Rust: in der Praxis

\n\n

Dependency Injection (DI) ist ein Entwurfsmuster, das es ermöglicht, Abhängigkeiten von außen an ein Objekt zu übergeben, anstatt sie innerhalb zu erstellen. In Rust ist dieser Ansatz dank des Ownership-Systems, Traits und Generics besonders nützlich. In diesem Artikel betrachten wir, wie man DI in Rust in der Praxis implementiert, von einfachen Beispielen bis hin zu fortgeschrittenen Techniken mit Dependency-Containern.

\n\n

Warum ist DI in Rust wichtig?

\n

In Rust, wo es keine klassische OOP mit Vererbung gibt, hilft DI:

\n
    \n
  • Kopplung reduzieren — Module hängen von Abstraktionen ab, nicht von konkreten Implementierungen.
  • \n
  • Testen vereinfachen — Sie können reale Implementierungen durch Mocks ersetzen.
  • \n
  • Flexibilität erhöhen — Das Verhalten des Systems kann leicht geändert werden, ohne dessen Code zu ändern.
  • \n
\n\n

Grundlagen: Abhängigkeiten über Parameter übergeben

\n

Der einfachste Weg der Abhängigkeitsinjektion ist die Übergabe als Parameter einer Funktion oder des Konstruktors einer Struktur.

\n\n
// Definieren eines Traits für die Abstraktion\ntrait Logger {\n    fn log(&self, message: &str);\n}\n\n// Implementierung für die Konsole\nstruct ConsoleLogger;\n\nimpl Logger for ConsoleLogger {\n    fn log(&self, message: &str) {\n        println!("LOG: {}", message);\n    }\n}\n\n// Implementierung für eine Datei\nstruct FileLogger {\n    filename: String,\n}\n\nimpl Logger for FileLogger {\n    fn log(&self, message: &str) {\n        // Im echten Code würde hier in eine Datei geschrieben werden\n        println!("FILE: {} -> {}", self.filename, message);\n    }\n}\n\n// Dienst, der DI über den Konstruktor verwendet\nstruct UserService<L: Logger> {\n    logger: L,\n}\n\nimpl<L: Logger> UserService<L> {\n    fn new(logger: L) -> Self {\n        Self { logger }\n    }\n\n    fn create_user(&self, name: &str) {\n        self.logger.log(&format!("User created: {}", name));\n    }\n}\n\nfn main() {\n    let console_logger = ConsoleLogger;\n    let user_service = UserService::new(console_logger);\n    user_service.create_user("Alice");\n}\n
\n\n

In diesem Beispiel weiß UserService nicht, welcher Logger genau verwendet wird – er hängt nur vom Trait Logger ab. Das ist die Essenz von DI.

\n\n

Fortgeschrittener Ansatz: Trait-Objekte und dynamische Dispatch

\n

Wenn Sie verschiedene Implementierungen in einem Container speichern müssen, verwenden Sie Trait-Objekte (Box<dyn Trait>).

\n\n
trait Database {\n    fn query(&self, sql: &str) -> Vec<String>;\n}\n\nstruct PostgresDb;\nimpl Database for PostgresDb {\n    fn query(&self, sql: &str) -> Vec<String> {\n        vec!["result1".to_string(), "result2".to_string()]\n    }\n}\n\nstruct SqliteDb;\nimpl Database for SqliteDb {\n    fn query(&self, sql: &str) -> Vec<String> {\n        vec!["sqlite_result".to_string()]\n    }\n}\n\nstruct App {\n    db: Box<dyn Database>,\n    logger: Box<dyn Logger>,\n}\n\nimpl App {\n    fn new(db: Box<dyn Database>, logger: Box<dyn Logger>) -> Self {\n        Self { db, logger }\n    }\n\n    fn run(&self) {\n        let results = self.db.query("SELECT * FROM users");\n        for r in results {\n            self.logger.log(&r);\n        }\n    }\n}\n\nfn main() {\n    let app = App::new(\n        Box::new(PostgresDb),\n        Box::new(ConsoleLogger),\n    );\n    app.run();\n}\n
\n\n

Der Vorteil von Trait-Objekten ist, dass Sie die Implementierungen zur Laufzeit ändern können. Der Nachteil ist ein geringer Overhead durch dynamischen Dispatch.

\n\n

Dependency-Container: Inversion of Control

\n

Für größere Projekte ist die Verwendung eines Dependency-Containers praktisch. Obwohl es in Rust keinen standardmäßigen DI-Container gibt, lässt er sich leicht selbst implementieren oder es können Crates wie shaku oder dice verwendet werden.

\n\n

Beispiel für einen einfachen manuellen Container:

\n\n
use std::collections::HashMap;\nuse std::any::Any;\n\n// Ty                

Blogs

Buchempfehlungen