Introduction à l'Injection de Dépendances en C++
Dans le développement moderne en C++, l'un des principes clés pour créer un code flexible et testable est Dependency Injection (DI) — l'injection de dépendances. Ce modèle de conception permet de réduire le couplage entre les composants d'un système, rendant le code plus modulaire, réutilisable et facile à tester.
Dans cet article, nous allons voir ce qu'est l'Injection de Dépendances en C++, quels problèmes elle résout, examiner différentes façons de l'implémenter en pratique et fournir des exemples de code qui vous aideront à intégrer cette approche dans vos projets.
Le problème de la dépendance directe
Avant de parler de la solution, regardons un problème typique. Imaginez que nous avons une classe UserService qui utilise Logger pour écrire des logs :
class Logger {public: void log(const std::string& message) { std::cout << "[LOG]: " << message << std::endl; }};
class UserService {private: Logger logger; // Dépendance rigidepublic: void createUser(const std::string& name) { logger.log("Creating user: " + name); // ... logique de création d'utilisateur }};Ici, UserService crée lui-même une instance de Logger. Cela crée plusieurs problèmes :
- Difficulté de test : impossible de remplacer le logger par un objet mock lors des tests unitaires.
- Couplage fort : modifier le constructeur de
Loggernécessitera de modifier toutes les classes qui l'utilisent. - Violation du SRP (Single Responsibility Principle) : la classe est responsable non seulement de sa propre logique, mais aussi de la création de ses dépendances.
Qu'est-ce que l'Injection de Dépendances ?
Dependency Injection est un modèle dans lequel un objet reçoit ses dépendances de l'extérieur, plutôt que de les créer lui-même. L'idée principale est : "ne demande pas, reçois".
En C++, il existe trois façons principales d'injecter des dépendances :
- Constructor Injection — via le constructeur.
- Setter Injection — via des méthodes setter.
- Interface Injection — via une interface.
1. Constructor Injection (Injection via le constructeur)
C'est la méthode la plus courante et préférée. Les dépendances sont passées au constructeur de la classe :
#include <memory>#include <iostream>
class ILogger {public: virtual ~ILogger() = default; virtual void log(const std::string& message) = 0;};
class ConsoleLogger : public ILogger {public: void log(const std::string& message) override { std::cout << "[CONSOLE]: " << message << std::endl; }};
class FileLogger : public ILogger {public: void log(const std::string& message) override { // Logique d'écriture dans un fichier std::cout << "[FILE]: " << message << std::endl; }};
class UserService {private: std::unique_ptr<ILogger> logger_; // Dépendance via interfacepublic: // Constructor Injection explicit UserService(std::unique_ptr<ILogger> logger) : logger_(std::move(logger)) {}
void createUser(const std::string& name) { logger_->log("Creating user: " + name); // ... logique }};
int main() { auto consoleLogger = std::make_unique<ConsoleLogger>(); UserService service(std::move(consoleLogger)); service.createUser("Alice"); return 0;}Avantages :
- La dépendance est clairement visible dans la signature du constructeur.
- L'objet est toujours dans un état valide après sa création.
- Facile à tester — on peut ins