La inyección de dependencias es una técnica por la que un objeto recibe desde fuera los colaboradores que necesita. La inyección por constructor es su forma más habitual, pero no exige usar un contenedor.
Conviene distinguirla de IoC y DIP. Son ideas relacionadas, pero no significan lo mismo.
El Problema: “Yo me lo guiso, yo me lo como”
Imagina una clase ServicioUsuario que necesita guardar datos. Una implementación acoplada podría hacer esto:
// ❌ MAL: Alto Acoplamiento
public class ServicioUsuario
{
private FileLogger _logger;
private SqlDatabase _database;
public ServicioUsuario()
{
// EL PECADO ORIGINAL: Usar 'new' dentro de la clase
_logger = new FileLogger("log.txt");
_database = new SqlDatabase("connectionString");
}
public void Registrar()
{
_logger.Log("Registrando...");
_database.Guardar();
}
}¿Por qué es terrible esto?
- Rigidez:
ServicioUsuarioestá casado para siempre conFileLoggerySqlDatabase. Si quieres cambiar el log a la consola o la base de datos a la nube, tienes que modificar esta clase. - Difícil de probar de forma aislada: al instanciar
ServicioUsuario, intenta conectarse a la base de datos real y escribir en disco.
La Solución: “No me llames, yo te llamo”
La Inyección de Dependencias nos dice: Una clase no debe crear sus dependencias. Debe pedirlas.
En lugar de fabricar el Logger dentro, le decimos al mundo: “Oye, yo necesito un Logger para funcionar. Dámelo tú”.
// ✅ BIEN: Inyección de Dependencias
public class ServicioUsuario
{
private ILogger _logger;
private IDatabase _database;
// Inyección por Constructor: "Dame lo que necesito"
public ServicioUsuario(ILogger logger, IDatabase database)
{
_logger = logger;
_database = database;
}
public void Registrar()
{
_logger.Log("Registrando...");
_database.Guardar();
}
}Ahora ServicioUsuario no sabe qué logger usa. ¿Es un archivo? ¿Es consola? ¿Es un mock falso para tests? Le da igual. Solo sabe que implementa ILogger.
Desenredando la Sopa de Letras: DI vs DIP vs IoC
Aquí es donde todo el mundo se lía. Vamos a distinguir los tres términos clave:
Es el principio (La ‘D’ de SOLID). Es una regla teórica.
Dice: “Los módulos de alto nivel no deben depender de los de bajo nivel. Ambos deben depender de abstracciones”.
En el ejemplo: ServicioUsuario no debe depender de FileLogger, sino de ILogger.
Es el concepto general. Dice: Invertir el flujo de control. En lugar de que tu clase controle la creación de objetos, algo externo (el framework, el main) lo controla.
Es la TÉCNICA concreta. Dice: “La forma de lograr IoC y cumplir DIP es pasando las dependencias a través del constructor (o propiedades)”.
Resumen en una frase: Usamos la técnica de Inyección de Dependencias para aplicar el principio de Inversión de Dependencias.
Tipos de Inyección
Existen principalmente tres formas de inyectar dependencias:
Inyección por Constructor (La recomendada)
Es la que hemos visto. Las dependencias se declaran en el constructor.
- Ventaja: obliga a proporcionar las dependencias al crear el objeto. Conviene validarlas en el constructor para rechazar valores nulos.
- Uso: En el 99% de los casos.
Inyección por Propiedad (Setter Injection)
public class Cliente
{
public ILogger Logger { get; set; } // Opcional
}- Ventaja: Permite dependencias opcionales.
- Desventaja: Puedes olvidarte de setearla y tener un
NullReferenceExceptionen tiempo de ejecución.
Inyección por Método
Pasar la dependencia solo al método que la necesita.
public void GenerarReporte(IGeneradorPDF generador) { ... }¿Quién crea los objetos entonces? (DI Containers)
Si ServicioUsuario no crea el Logger, ¿quién lo hace? Alguien tiene que hacer el new.
En una aplicación pequeña, lo hacemos en el “Entry Point” (Main):
// Pure DI (Inyección manual)
void Main()
{
ILogger logger = new ConsoleLogger();
IDatabase db = new MySqlDatabase();
// Nosotros hacemos de "pegamento"
var servicio = new ServicioUsuario(logger, db);
servicio.Registrar();
}Pero en aplicaciones grandes (como ASP.NET Core), usamos un Contenedor de Inyección de Dependencias (DI Container). El contenedor es una lista inteligente donde registramos nuestras clases y él se encarga de resolver las dependencias automáticamente.
// Ejemplo conceptual en .NET Core
builder.Services.AddTransient<ILogger, ConsoleLogger>();
builder.Services.AddTransient<IDatabase, MySqlDatabase>();
builder.Services.AddTransient<ServicioUsuario>();
// ... más tarde ...
// El contenedor ve que ServicioUsuario necesita ILogger e IDatabase,
// los busca, los crea y te devuelve el servicio montado.
var servicio = contenedor.GetService<ServicioUsuario>();El Poder del Testing
La mayor ventaja inmediata de la DI es la facilidad para hacer pruebas unitarias.
Como ServicioUsuario pide interfaces, en nuestros tests podemos pasarle Mocks (objetos falsos).
[Test]
public void Test_Registrar_LlamaAGuardar()
{
// Arrange: Creamos dependencias falsas
var mockLogger = new Mock<ILogger>();
var mockDb = new Mock<IDatabase>();
var servicio = new ServicioUsuario(mockLogger.Object, mockDb.Object);
// Act
servicio.Registrar();
// Assert: Verificamos que se llamó a Guardar() sin tocar una BBDD real
mockDb.Verify(db => db.Guardar(), Times.Once);
}La Inyección de Dependencias no es una moda, es una necesidad para cualquier software profesional. Nos permite escribir código:
- Desacoplado: Las piezas son intercambiables.
- Mantenible: Cambiar una implementación no rompe el sistema.
- Testable: Podemos aislar cada clase para probarla.