Container de Injeção de Dependência para C++: visão geral da biblioteca Fruit
No desenvolvimento moderno em C++, o gerenciamento de dependências entre classes torna-se criticamente importante para criar aplicações flexíveis, testáveis e de fácil manutenção. O padrão Dependency Injection (DI) permite externalizar a criação de objetos para fora das classes, e o container DI automatiza esse processo. Uma das melhores bibliotecas para implementar injeção de dependências em C++ é o Fruit — um container leve, rápido e type-safe que opera em tempo de compilação.
O que é um container de Injeção de Dependência?
Um container de Injeção de Dependência é uma ferramenta que assume a responsabilidade de criar e vincular objetos (dependências). Em vez de a própria classe criar suas dependências via new ou fábricas, o container as "injeta" externamente. Isso implementa o princípio Inversion of Control (IoC) e torna o código fracamente acoplado.
Por que Fruit?
A biblioteca Fruit (https://github.com/google/fruit) foi desenvolvida no Google e se diferencia de alternativas (como Boost.DI) pelas seguintes características:
- Verificação em tempo de compilação: todas as ligações são verificadas em tempo de compilação, eliminando erros de execução relacionados à falta de dependências.
- Overhead mínimo: Fruit não usa RTTI nem chamadas virtuais, gerando código eficiente.
- Simplicidade: requer apenas C++14 e arquivos de cabeçalho — sem macros ou geração de código.
- Suporte a componentes: é possível montar o grafo de dependências a partir de módulos reutilizáveis.
Instalação
Fruit é distribuído como uma biblioteca header-only (requer apenas fruit.h). Para instalação via vcpkg:
vcpkg install fruitOu clone o repositório e inclua o arquivo include/fruit/fruit.h.
Conceitos principais
No Fruit, são utilizados três elementos-chave:
- Injetor (Injector) — o container que cria os objetos.
- Componente (Component) — módulo que registra os tipos e suas fábricas.
- Anotações — para diferenciar diferentes implementações de uma mesma interface.
Exemplo de uso
Vamos considerar um exemplo clássico: um serviço depende de um repositório. Criaremos uma interface e duas implementações:
// 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;};Implementações:
// implementations.cpp#include "interfaces.hpp"#include <iostream>
class ConsoleLogger : public ILogger {public: INJECT(ConsoleLogger()) = default; // Anotação Fruit para o construtor 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); }};Serviço que utiliza ambas as dependências:
// 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); }};Agora vamos montar o componente