Introduction : Pourquoi l'injection de dépendances est-elle importante en Rust ?
\n\nLors de l'écriture de programmes en Rust, vous rencontrerez tôt ou tard une situation où un module (par exemple, un service ou un contrôleur) doit utiliser les fonctionnalités d'un autre module (base de données, API, journaliseur). La méthode la plus simple consiste à créer l'objet nécessaire directement à l'intérieur du code qui l'utilise :
\n\nstruct UserService;\n\nimpl UserService {\n fn get_user(&self, id: u32) -> String {\n let db = Database::new(); // ❌ Couplage fort\n db.query_user(id)\n }\n}\n\n\nCette approche crée un couplage fort (tight coupling). Le code devient difficile à tester, à réutiliser et à modifier. L'injection de dépendances (DI) est un modèle dans lequel les dépendances sont transmises à l'objet de l'extérieur, plutôt que créées à l'intérieur de celui-ci. Rust, étant un langage doté d'un système de possession et de traits, offre des moyens élégants d'implémenter la DI sans frameworks.
\n\nDans cet article, nous allons voir comment injecter des dépendances en Rust avec des exemples pratiques : de la simple transmission de structures à l'utilisation de traits et de pointeurs intelligents.
\n\n1. Approche de base : Transmission des dépendances via le constructeur
\n\nLa façon la plus simple d'injecter une dépendance en Rust est de la passer en argument au constructeur ou à la fonction. Prenons l'exemple d'un journaliseur.
\n\nMauvais exemple (sans 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, // ❌ Liaison rigide à un type spécifique\n}\n\nimpl OrderService {\n fn new() -> Self {\n Self {\n logger: ConsoleLogger, // Créé à l'intérieur\n }\n }\n\n fn place_order(&self, item: &str) {\n self.logger.log(&format!("Commande : {}", item));\n }\n}\n\n\nBon exemple (avec 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, // La dépendance est transmise de l'extérieur\n}\n\nimpl OrderService {\n // Le constructeur accepte la dépendance\n fn new(logger: ConsoleLogger) -> Self {\n Self { logger }\n }\n\n fn place_order(&self, item: &str) {\n self.logger.log(&format!("Commande : {}", item));\n }\n}\n\nfn main() {\n let logger = ConsoleLogger;\n let service = OrderService::new(logger);\n service.place_order("Livre sur Rust");\n}\n\n\nQu'est-ce qui a changé ? Désormais, OrderService ne crée pas lui-même le journaliseur. Il le reçoit tout prêt. C'est déjà de la DI, mais il y a une nuance : nous sommes toujours liés au type spécifique ConsoleLogger. Pour rendre le code flexible, il faut utiliser des traits.
2. Utilisation des traits pour l'abstraction des dépendances
\n\nLes traits en Rust sont l'équivalent des interfaces dans d'autres langages. Ils permettent de définir un contrat que les dépendances doivent respecter.
\n\nDéfinir le trait Logger
\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!("[Console] : {}", msg);\n }\n}\n\nstruct FileLogger {\n filename: String,\n}\nimpl Logger for FileLogger {\n fn log(&self, msg: &str) {\n // Simulation d'écriture dans un fichier\n println!("[Fichier {}] : {}", self.filename, msg);\n }\n}\n\n\nInjecter le trait via les génériques (generics)
\n\nstruct OrderService<L: Logger> {\n logger: L, // Type générique implémentant le trait Logger\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!("Commande : {}", item));\n }\n}\n\nfn main() {\n let console_logge