Injeção de Dependência em C++ na prática: exemplos e padrões

Treinador Online de Python para Iniciantes

Aprenda Python facilmente sem sobrecarga teórica. Resolva tarefas práticas com verificação automática e escreva código diretamente no navegador.

Start Course

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

Blogs

Book Recommendations