Injection de Dépendances en Rust : en pratique
\n\nL'Injection de Dépendances (DI) est un modèle de conception qui permet de transmettre les dépendances à un objet depuis l'extérieur, plutôt que de les créer à l'intérieur. En Rust, cette approche est particulièrement utile grâce au système de possession, aux traits et aux génériques. Dans cet article, nous verrons comment implémenter la DI en Rust en pratique, des exemples simples aux techniques avancées avec des conteneurs de dépendances.
\n\nPourquoi la DI est-elle importante en Rust ?
\nEn Rust, où il n'y a pas de POO classique avec héritage, la DI aide à :
\n- \n
- Réduire le couplage — les modules dépendent d'abstractions, et non d'implémentations concrètes. \n
- Simplifier les tests — vous pouvez remplacer les implémentations réelles par des mocks. \n
- Augmenter la flexibilité — il est facile de modifier le comportement du système sans changer son code. \n
Les bases : transmission des dépendances via les paramètres
\nLa façon la plus simple d'injecter une dépendance est de la transmettre en tant que paramètre d'une fonction ou d'un constructeur de structure.
\n\n// Définir un trait pour l'abstraction\ntrait Logger {\n fn log(&self, message: &str);\n}\n\n// Implémentation pour la console\nstruct ConsoleLogger;\n\nimpl Logger for ConsoleLogger {\n fn log(&self, message: &str) {\n println!("LOG: {}", message);\n }\n}\n\n// Implémentation pour un fichier\nstruct FileLogger {\n filename: String,\n}\n\nimpl Logger for FileLogger {\n fn log(&self, message: &str) {\n // Dans le code réel, il y aurait une écriture dans un fichier\n println!("FILE: {} -> {}", self.filename, message);\n }\n}\n\n// Service utilisant la DI via le constructeur\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\nDans cet exemple, UserService ne sait pas quel logger est utilisé — il dépend uniquement du trait Logger. C'est l'essence même de la DI.
Approche avancée : objets traits et dispatch dynamique
\nLorsque vous devez stocker différentes implémentations dans un même conteneur, utilisez les objets traits (Box<dyn Trait>).
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\nL'avantage des objets traits est que vous pouvez changer les implémentations à l'exécution. L'inconvénient est un léger surcoût dû au dispatch dynamique.
\n\nConteneur de dépendances : inversion de contrôle
\nPour les grands projets, il est pratique d'utiliser un conteneur de dépendances. Bien qu'il n'y ait pas de conteneur DI standard en Rust, il est facile de l'implémenter soi-même ou d'utiliser des crates comme shaku ou dice.
Exemple d'un conteneur simple fait à la main :
\n\nuse std::collections::HashMap;\nuse std::any::Any;\n\n// Ty