Fundamentos de Inyección de dependencias
Parte de la sección Programación Orientada a Objetos del Journey de C# de Coddy — lección 46 de 70.
Inyección de Dependencias (DI) es una técnica en la que una clase recibe sus dependencias desde el exterior en lugar de crearlas ella misma. Esto hace que el código sea más flexible, fácil de probar y con un acoplamiento débil.
Sin DI, una clase crea sus propias dependencias, lo que dificulta su modificación o prueba:
public class OrderService
{
private EmailSender sender = new EmailSender(); // Fuertemente acoplado
public void PlaceOrder() => sender.Send("Order placed");
}Con DI, las dependencias se pasan a través del constructor:
public interface IMessageSender
{
void Send(string message);
}
public class EmailSender : IMessageSender
{
public void Send(string message) => Console.WriteLine("Email: " + message);
}
public class OrderService
{
private IMessageSender sender;
public OrderService(IMessageSender sender)
{
this.sender = sender;
}
public void PlaceOrder() => sender.Send("Order placed");
}Ahora OrderService no sabe ni le importa si está usando correo electrónico, SMS o un remitente simulado para pruebas. La dependencia se "inyecta" cuando se crea el objeto:
IMessageSender emailSender = new EmailSender();
OrderService service = new OrderService(emailSender);
service.PlaceOrder(); // Email: Pedido realizadoEste patrón sigue el principio de depender de abstracciones (interfaces) en lugar de implementaciones concretas. Permite intercambiar implementaciones sin modificar la clase que las utiliza, lo cual es esencial para las pruebas unitarias y para mantener arquitecturas flexibles.
Desafío
FácilVamos a construir un sistema de registro (logging) que demuestre el poder de la inyección de dependencias. En lugar de codificar de forma rígida cómo se escriben los registros, crearás una clase Logger flexible que pueda funcionar con cualquier destino de registro (consola, simulación de archivo o cualquier otra cosa) sin conocer los detalles específicos.
Organizarás tu código en tres archivos:
ILogWriter.cs: Define una interfaz llamadaILogWriteren el espacio de nombresLogging. Esta interfaz representa el contrato para cualquier destino de registro y debe tener un único métodoWrite(string message)que devuelva una cadena describiendo dónde se escribió el mensaje.LogWriters.cs: Crea dos implementaciones concretas deILogWriteren el espacio de nombresLogging:ConsoleLogWriter: su métodoWritedevuelve"Console: " + messageFileLogWriter: recibe un nombre de archivo a través de su constructor y su métodoWritedevuelve"File[" + filename + "]: " + message
Logger.cs: Crea una claseLoggeren el espacio de nombresLoggingque reciba su dependencia a través de la inyección por constructor. ElLoggerdebe:- Aceptar un
ILogWritera través de su constructor y almacenarlo en un campo privado - Tener un método
Log(string message)que delegue en el escritor inyectado y devuelva el resultado
- Aceptar un
Program.cs: Demuestra la inyección de dependencias creando dos loggers diferentes: uno con unConsoleLogWritery otro con unFileLogWriter. Muestra cómo la misma claseLoggerse comporta de manera diferente según la dependencia que se inyecte.
Recibirás dos entradas:
- El nombre de archivo para el logger de archivo
- El mensaje a registrar
Crea un Logger con un ConsoleLogWriter y registra el mensaje. Luego crea otro Logger con un FileLogWriter (usando el nombre de archivo de la entrada) y registra el mismo mensaje. Imprime cada resultado en su propia línea.
Por ejemplo, si las entradas son app.log y System started, la salida debería ser:
Console: System started
File[app.log]: System startedObserva cómo la clase Logger nunca cambia: funciona con cualquier implementación de ILogWriter. Esta es la esencia de la inyección de dependencias: el Logger depende de una abstracción (la interfaz), y la implementación concreta se "inyecta" desde el exterior. ¡Podrías agregar fácilmente un DatabaseLogWriter o CloudLogWriter más tarde sin modificar la clase Logger en absoluto!
Hoja de referencia
La Inyección de Dependencias (DI) es una técnica en la que una clase recibe sus dependencias desde el exterior en lugar de crearlas ella misma, lo que hace que el código sea más flexible, testable y débilmente acoplado.
Sin DI (fuertemente acoplado):
public class OrderService
{
private EmailSender sender = new EmailSender(); // Fuertemente acoplado
public void PlaceOrder() => sender.Send("Order placed");
}Con DI (débilmente acoplado):
public interface IMessageSender
{
void Send(string message);
}
public class EmailSender : IMessageSender
{
public void Send(string message) => Console.WriteLine("Email: " + message);
}
public class OrderService
{
private IMessageSender sender;
public OrderService(IMessageSender sender)
{
this.sender = sender;
}
public void PlaceOrder() => sender.Send("Order placed");
}Inyectando la dependencia:
IMessageSender emailSender = new EmailSender();
OrderService service = new OrderService(emailSender);
service.PlaceOrder(); // Email: Order placedLa DI sigue el principio de depender de abstracciones (interfaces) en lugar de implementaciones concretas, lo que permite intercambiar implementaciones sin modificar la clase que las utiliza.
Pruébalo tú mismo
using System;
using Logging;
class Program
{
public static void Main(string[] args)
{
// Leer entradas
string filename = Console.ReadLine();
string message = Console.ReadLine();
// TODO: Crear un Logger con un ConsoleLogWriter y registrar el mensaje
// Imprimir el resultado
// TODO: Crear otro Logger con un FileLogWriter (usando el filename) y registrar el mismo mensaje
// Imprimir el resultado
}
}
Esta lección incluye un breve cuestionario. Empieza la lección para responderlo y registrar tu progreso.
Todas las lecciones de Programación Orientada a Objetos
1Fundamentos de OOP
Archivos externosNamespaces y directivasIntro a clases y objetosLa palabra clave 'this'Métodos y parámetrosCampos vs PropiedadesConstructoresInicializadores de objetosResumen - Calculadora simple4Herencia
Sintaxis básica de herencia (:)La palabra clave 'base'Palabras clave Virtual y OverrideClases selladas (Sealed)La clase base 'object'Resumen: Jerarquía de empleados7Características avanzadas
Sobrecarga de operadoresIndexadores (this[])Sobrescritura de ToString()Métodos de extensiónResumen - Lista personalizada10Patrones de diseño - Parte 1
Introducción a los patrones de diseñoSingleton Thread-SafePatrón FactoryPatrón Observer (Eventos)Patrón Strategy2Propiedades y miembros estáticos
Propiedades autoimplementadasPropiedades de solo lectura y escrituraCampos y métodos estáticosClases estáticasMiembros con cuerpo de expresión5Polimorfismo e Interfaces
Polimorfismo: Compilación vs. EjecuciónInterfaz vs. Clase AbstractaInterfaces MúltiplesInterfaces ExplícitasUpcasting y DowncastingRepaso: Calculadora de Figuras8Conceptos avanzados de POO
Composición sobre herenciaGenéricos (clases y métodos)Delegados y eventosAtributos y ReflexiónIDisposable y la sentencia usingFundamentos de Inyección de dependencias11Patrones de diseño, parte 2
Patrón CommandPatrón AdapterPatrón DecoratorPatrón Template MethodPatrón StatePatrón Composite3Arquitectura de clases
Datos de instancia vs. estáticosPalabras clave 'readonly' y 'const'Campos de respaldoResumen - Gestor de cuentas bancarias6Encapsulamiento
Modificadores de accesoPropiedades para el encapsulamientoImplementación de ocultamiento de datosPatrones de inmutabilidadRepaso - Registros de estudiantes