Introducción: ¿por qué es importante la Inyección de Dependencias en C++?
En el desarrollo moderno de C++, el soporte de un acoplamiento débil (loose coupling) entre clases es la clave para la flexibilidad y la capacidad de prueba del código. Dependency Injection (DI) o inyección de dependencias es un patrón en el cual un objeto recibe sus dependencias desde el exterior, en lugar de crearlas él mismo. Esto implementa directamente el principio de inversión de dependencias (Dependency Inversion Principle) de SOLID.
Sin DI, las clases a menudo están rígidamente vinculadas a implementaciones concretas:
// Mal: la clase crea la dependencia por sí mismaclass ReportGenerator { Database db_; // Acoplamiento rígido a una BD específicapublic: ReportGenerator() : db_("localhost") {} void generate() { db_.query("SELECT ..."); }};Con DI obtenemos flexibilidad, la posibilidad de intercambiar implementaciones (por ejemplo, para pruebas unitarias) y simplificamos el mantenimiento del código. En este artículo analizaremos tres formas prácticas de implementar DI en C++: a través del constructor, a través de interfaces y con un contenedor simple.
1. Inyección a través del constructor (Constructor Injection)
La forma más común y recomendada. Las dependencias se pasan al constructor de la clase como parámetros. En C++, para esto es conveniente usar punteros inteligentes (std::unique_ptr, std::shared_ptr) o referencias.
Ejemplo: logger y servicio
#include <iostream>#include <memory>#include <string>
// 1. Abstracción (interfaz)class ILogger {public: virtual ~ILogger() = default; virtual void log(const std::string& msg) = 0;};
// 2. Implementación concretaclass ConsoleLogger : public ILogger {public: void log(const std::string& msg) override { std::cout << "[LOG]: " << msg << std::endl; }};
// 3. Clase que depende de ILoggerclass UserService { std::shared_ptr<ILogger> logger_; // Dependenciapublic: // Inyección a través del constructor explicit UserService(std::shared_ptr<ILogger> logger) : logger_(std::move(logger)) {}
void createUser(const std::string& name) { // Lógica de negocio... logger_->log("User created: " + name); }};
int main() { auto logger = std::make_shared<ConsoleLogger>(); UserService service(logger); service.createUser("Alice"); return 0;}Ventajas:
- La dependencia es explícitamente visible en la firma del constructor.
- El objeto siempre está en un estado válido (después de la creación).
- Es fácil intercambiar la implementación para pruebas (por ejemplo,
MockLogger).
Desventaja: con una gran cantidad de dependencias, el constructor puede volverse voluminoso. En tales casos, los contenedores ayudan (ver sección 3).
2. Inyección a través de interfaces y polimorfismo
Esta es una evolución del enfoque anterior. Definimos interfaces virtuales puras para todas las dependencias. El código cliente trabaja solo con abstracciones, y las implementaciones concretas se proporcionan en la etapa de construcción del objeto (composition root).
Ejemplo: repositorio y lógica de negocio
#include <vector>#include <memory>
// Interfaz del repositorioclass IUserRepository {public: virtual ~IUserRepository() = default; virtual void add(const std::string& user) = 0; virtual std::vector<std::string> getAll() const = 0;};
// Implementación para base de datosclass 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 negocio que no conoce la BDclass UserManager { std::uniq