Introdução à Dependency Injection em C++
No desenvolvimento moderno em C++, um dos princípios-chave para criar código flexível e testável é a Dependency Injection (DI) — injeção de dependências. Este padrão de projeto permite reduzir o acoplamento entre os componentes do sistema, tornando o código mais modular, reutilizável e fácil de testar.
Neste artigo, vamos analisar o que é Dependency Injection em C++, quais problemas ela resolve, explorar diferentes maneiras de implementá-la na prática e fornecer exemplos de código que ajudarão você a aplicar essa abordagem em seus projetos.
O problema da dependência direta
Antes de falarmos sobre a solução, vamos observar um problema típico. Imagine que temos uma classe UserService que usa um Logger para registrar logs:
class Logger {public: void log(const std::string& message) { std::cout << "[LOG]: " << message << std::endl; }};
class UserService {private: Logger logger; // Dependência rígidapublic: void createUser(const std::string& name) { logger.log("Creating user: " + name); // ... lógica de criação de usuário }};Aqui, UserService cria sua própria instância de Logger. Isso gera vários problemas:
- Dificuldade de teste: não é possível substituir o logger por um mock-object durante os testes unitários.
- Alto acoplamento: alterar o construtor de
Loggerexigirá modificar todas as classes que o utilizam. - Violação do SRP (Single Responsibility Principle): a classe é responsável não apenas por sua lógica, mas também pela criação de dependências.
O que é Dependency Injection?
Dependency Injection é um padrão no qual um objeto recebe suas dependências externamente, em vez de criá-las por conta própria. A ideia principal é: "não peça, receba".
Em C++, existem três maneiras principais de injetar dependências:
- Constructor Injection — através do construtor.
- Setter Injection — através de métodos setter.
- Interface Injection — através de uma interface.
1. Constructor Injection (Injeção via construtor)
Esta é a maneira mais comum e preferida. As dependências são passadas para o construtor da 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 { // Lógica de escrita em arquivo std::cout << "[FILE]: " << message << std::endl; }};
class UserService {private: std::unique_ptr<ILogger> logger_; // Dependência 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); // ... lógica }};
int main() { auto consoleLogger = std::make_unique<ConsoleLogger>(); UserService service(std::move(consoleLogger)); 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 de testar — é possível subst