Conteneur d'injection de dépendances pour C++ : présentation de la bibliothèque Fruit
Dans le développement C++ moderne, la gestion des dépendances entre classes devient cruciale pour créer des applications flexibles, testables et maintenables. Le modèle Dependency Injection (DI) permet de déplacer la création d'objets en dehors des classes, et le conteneur DI automatise ce processus. L'une des meilleures bibliothèques pour implémenter l'injection de dépendances en C++ est Fruit — un conteneur léger, rapide et type-safe fonctionnant à la compilation.
Qu'est-ce qu'un conteneur d'injection de dépendances ?
Un conteneur d'injection de dépendances est un outil qui prend en charge la création et la liaison des objets (dépendances). Au lieu que la classe crée elle-même ses dépendances via new ou des fabriques, le conteneur les « injecte » de l'extérieur. Cela implémente le principe d'Inversion de Contrôle (IoC) et rend le code faiblement couplé.
Pourquoi Fruit ?
La bibliothèque Fruit (https://github.com/google/fruit) a été développée chez Google et se distingue des alternatives (comme Boost.DI) par les caractéristiques suivantes :
- Vérification à la compilation : toutes les liaisons sont vérifiées à la compilation, ce qui élimine les erreurs d'exécution liées à l'absence de dépendances.
- Surcharge minimale : Fruit n'utilise pas RTTI ni d'appels virtuels, générant un code efficace.
- Simplicité : nécessite uniquement C++14 et des fichiers d'en-tête — pas de macros ni de génération de code.
- Support des composants : possibilité de construire un graphe de dépendances à partir de modules réutilisables.
Installation
Fruit est distribué comme une bibliothèque header-only (nécessite uniquement fruit.h). Pour l'installation via vcpkg :
vcpkg install fruitOu clonez le dépôt et incluez le fichier include/fruit/fruit.h.
Concepts principaux
Fruit utilise trois éléments clés :
- Injecteur (Injector) — le conteneur qui crée les objets.
- Composant (Component) — un module qui enregistre les types et leurs fabriques.
- Annotations — pour distinguer différentes implémentations d'une même interface.
Exemple d'utilisation
Considérons un exemple classique : un service dépend d'un dépôt. Créons une interface et deux implémentations :
// interfaces.hpp#include <fruit/fruit.h>#include <string>
class ILogger {public: virtual void log(const std::string& msg) = 0; virtual ~ILogger() = default;};
class IUserRepository {public: virtual std::string getUser(int id) = 0; virtual ~IUserRepository() = default;};Implémentations :
// implementations.cpp#include "interfaces.hpp"#include <iostream>
class ConsoleLogger : public ILogger {public: INJECT(ConsoleLogger()) = default; // Annotation Fruit pour le constructeur void log(const std::string& msg) override { std::cout << "[LOG] " << msg << std::endl; }};
class SqlRepository : public IUserRepository {public: INJECT(SqlRepository()) = default; std::string getUser(int id) override { return "User_" + std::to_string(id); }};Service utilisant les deux dépendances :
// user_service.cpp#include "interfaces.hpp"
class UserService {private: ILogger* logger; IUserRepository* repo;public: INJECT(UserService(ILogger* logger, IUserRepository* repo)) : logger(logger), repo(repo) {} void processUser(int id) { auto name = repo->getUser(id); logger->log("Processing user: " + name); }};Maintenant, assemblons le composant
Aussi dans la bibliothèque
Travailler avec YAML en JavaScript (Node.js) : parsing, génération et validation
Envoi d'Email en C# (.NET) : bibliothèque System.Net.Mail
Travailler avec les dates en Rust : aperçu des bibliothèques pour le temps et le calendrier
ts-pattern TypeScript : guide complet avec exemples pratiques