Introducción a Dependency Injection en C++
En el desarrollo moderno con C++, uno de los principios clave para crear código flexible y comprobable es Dependency Injection (DI) — inyección de dependencias. Este patrón de diseño permite reducir el acoplamiento entre los componentes del sistema, haciendo que el código sea más modular, reutilizable y fácil de probar.
En este artículo, analizaremos qué es Dependency Injection en C++, qué problemas resuelve, exploraremos diferentes formas de implementarlo en la práctica y proporcionaremos ejemplos de código que le ayudarán a incorporar este enfoque en sus proyectos.
El problema de la dependencia directa
Antes de hablar de la solución, veamos un problema típico. Imagine que tenemos una clase UserService que utiliza Logger para registrar logs:
class Logger {public: void log(const std::string& message) { std::cout << "[LOG]: " << message << std::endl; }};
class UserService {private: Logger logger; // Dependencia rígidapublic: void createUser(const std::string& name) { logger.log("Creating user: " + name); // ... lógica de creación de usuario }};Aquí, UserService crea su propia instancia de Logger. Esto genera varios problemas:
- Dificultad para realizar pruebas: es imposible reemplazar el logger por un objeto mock durante las pruebas unitarias.
- Alto acoplamiento: cambiar el constructor de
Loggerrequerirá modificar todas las clases que lo utilizan. - Violación del SRP (Single Responsibility Principle): la clase no solo se encarga de su propia lógica, sino también de crear sus dependencias.
¿Qué es Dependency Injection?
Dependency Injection es un patrón en el cual un objeto recibe sus dependencias desde el exterior, en lugar de crearlas por sí mismo. La idea principal es: "no preguntes, recibe".
En C++, existen tres formas principales de inyectar dependencias:
- Constructor Injection — a través del constructor.
- Setter Injection — a través de métodos setter.
- Interface Injection — a través de una interfaz.
1. Constructor Injection (Inyección a través del constructor)
Esta es la forma más común y preferida. Las dependencias se pasan al constructor de la clase:
#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 escritura en archivo std::cout << "[FILE]: " << message << std::endl; }};
class UserService {private: std::unique_ptr<ILogger> logger_; // Dependencia a través de interfazpublic: // 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;}Ventajas:
- La dependencia es explícitamente visible en la firma del constructor.
- El objeto siempre se encuentra en un estado válido después de su creación.
- Fácil de probar: se puede sustituir