Introdução: por que a Injeção de Dependência é importante em C++?
No desenvolvimento moderno em C++, o suporte ao baixo acoplamento (loose coupling) entre classes é a chave para a flexibilidade e testabilidade do código. Dependency Injection (DI) ou Injeção de Dependência é um padrão no qual um objeto recebe suas dependências externamente, em vez de criá-las ele mesmo. Isso implementa diretamente o princípio de inversão de dependências (Dependency Inversion Principle) do SOLID.
Sem DI, as classes frequentemente ficam rigidamente ligadas a implementações específicas:
// Ruim: a classe cria a dependência sozinhaclass ReportGenerator { Database db_; // Ligação rígida com um BD específicopublic: ReportGenerator() : db_("localhost") {} void generate() { db_.query("SELECT ..."); }};Com DI, ganhamos flexibilidade, a capacidade de substituir implementações (por exemplo, para testes unitários) e simplificamos a manutenção do código. Neste artigo, abordaremos três formas práticas de implementar DI em C++: via construtor, via interfaces e com um contêiner simples.
1. Injeção via construtor (Constructor Injection)
A forma mais comum e recomendada. As dependências são passadas para o construtor da classe como parâmetros. Em C++, é conveniente usar ponteiros inteligentes (std::unique_ptr, std::shared_ptr) ou referências para isso.
Exemplo: logger e serviço
#include <iostream>#include <memory>#include <string>
// 1. Abstração (interface)class ILogger {public: virtual ~ILogger() = default; virtual void log(const std::string& msg) = 0;};
// 2. Implementação concretaclass ConsoleLogger : public ILogger {public: void log(const std::string& msg) override { std::cout << "[LOG]: " << msg << std::endl; }};
// 3. Classe que depende de ILoggerclass UserService { std::shared_ptr<ILogger> logger_; // Dependênciapublic: // Injeção via construtor explicit UserService(std::shared_ptr<ILogger> logger) : logger_(std::move(logger)) {}
void createUser(const std::string& name) { // Lógica de negócio... logger_->log("User created: " + name); }};
int main() { auto logger = std::make_shared<ConsoleLogger>(); UserService service(logger); service.createUser("Alice"); return 0;}Vantagens:
- A dependência é explicitamente visível na assinatura do construtor.
- O objeto está sempre em um estado válido (após a criação).
- Fácil substituir a implementação para testes (ex.:
MockLogger).
Desvantagem: com um grande número de dependências, o construtor pode se tornar extenso. Nesses casos, contêineres ajudam (veja a seção 3).
2. Injeção via interfaces e polimorfismo
Esta é uma evolução da abordagem anterior. Definimos interfaces puramente virtuais para todas as dependências. O código cliente trabalha apenas com abstrações, e as implementações concretas são fornecidas na etapa de montagem do objeto (composition root).
Exemplo: repositório e lógica de negócio
#include <vector>#include <memory>
// Interface do repositórioclass IUserRepository {public: virtual ~IUserRepository() = default; virtual void add(const std::string& user) = 0; virtual std::vector<std::string> getAll() const = 0;};
// Implementação para banco de dadosclass DatabaseUserRepo : public IUserRepository {public: void add(const std::string& user) override { // Consulta SQL } std::vector<std::string> getAll() const override { return {"Alice", "Bob"}; }};
// Lógica de negócio que não conhece o BDclass UserManager { std::uniq