Introduction : pourquoi l'injection de dépendances est-elle importante en C++ ?
\n\nDans le développement C++ moderne, le maintien d'un faible couplage (loose coupling) entre les classes est la clé de la flexibilité et de la testabilité du code. L'injection de dépendances (DI) 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. Cela met directement en œuvre le principe d'inversion des dépendances (Dependency Inversion Principle) de SOLID.
\n\nSans DI, les classes sont souvent fortement liées à des implémentations spécifiques :
\n\n// Mauvais : la classe crée elle-même sa dépendance\nclass ReportGenerator {\n Database db_; // Lien fort avec une base de données spécifique\npublic:\n ReportGenerator() : db_("localhost") {}\n void generate() { db_.query("SELECT ..."); }\n};\n\nAvec DI, nous obtenons de la flexibilité, la possibilité de remplacer les implémentations (par exemple, pour les tests unitaires) et nous simplifions la maintenance du code. Dans cet article, nous allons examiner trois méthodes pratiques pour implémenter la DI en C++ : via le constructeur, via les interfaces et à l'aide d'un conteneur simple.
\n\n1. Injection via le constructeur (Constructor Injection)
\n\nLa méthode la plus courante et recommandée. Les dépendances sont transmises au constructeur de la classe en tant que paramètres. En C++, il est pratique d'utiliser des pointeurs intelligents (std::unique_ptr, std::shared_ptr) ou des références.
Exemple : logger et service
\n\n#include <iostream>\n#include <memory>\n#include <string>\n\n// 1. Abstraction (interface)\nclass ILogger {\npublic:\n virtual ~ILogger() = default;\n virtual void log(const std::string& msg) = 0;\n};\n\n// 2. Implémentation concrète\nclass ConsoleLogger : public ILogger {\npublic:\n void log(const std::string& msg) override {\n std::cout << "[LOG]: " << msg << std::endl;\n }\n};\n\n// 3. Classe qui dépend de ILogger\nclass UserService {\n std::shared_ptr<ILogger> logger_; // Dépendance\npublic:\n // Injection via le constructeur\n explicit UserService(std::shared_ptr<ILogger> logger)\n : logger_(std::move(logger)) {}\n\n void createUser(const std::string& name) {\n // Logique métier...\n logger_->log("User created: " + name);\n }\n};\n\nint main() {\n auto logger = std::make_shared<ConsoleLogger>();\n UserService service(logger);\n service.createUser("Alice");\n return 0;\n}\n\nAvantages :
\n- \n
- La dépendance est clairement visible dans la signature du constructeur. \n
- L'objet est toujours dans un état valide (après sa création). \n
- Facile de remplacer l'implémentation pour les tests (par exemple,
MockLogger). \n
Inconvénient : avec un grand nombre de dépendances, le constructeur peut devenir lourd. Dans de tels cas, les conteneurs aident (voir la section 3).
\n\n2. Injection via les interfaces et le polymorphisme
\n\nIl s'agit d'un développement de l'approche précédente. Nous définissons des interfaces virtuelles pures pour toutes les dépendances. Le code client ne travaille qu'avec des abstractions, et les implémentations concrètes sont fournies au moment de la construction de l'objet (composition root).
\n\nExemple : dépôt et logique métier
\n\n#include <vector>\n#include <memory>\n\n// Interface du dépôt\nclass IUserRepository {\npublic:\n virtual ~IUserRepository() = default;\n virtual void add(const std::string& user) = 0;\n virtual std::vector<std::string> getAll() const = 0;\n};\n\n// Implémentation pour la base de données\nclass DatabaseUserRepo : public IUserRepository {\npublic:\n void add(const std::string& user) override {\n // Requête SQL\n }\n std::vector<std::string> getAll() const override {\n return {"Alice", "Bob"};\n }\n};\n\n// Logique métier, ignorant la base de données\nclass UserManager {\n std::uniq