Aplicación del Principio de Inversión de Dependencias en C#
Clase 14 de 16 • Curso de Principios SOLID en C# y .NET
Resumen
¿Cómo funciona el principio de inversión de dependencias en la configuración del código?
El principio de inversión de dependencias es fundamental para crear un código más flexible y mantenible. En este contexto, se analiza una API que utiliza un StudentController
en el que se evidencian dependencias directas con StudentRepository
y Logbook
. Estas dependencias directas violan el principio de inversión de dependencias porque implican una fuerte vinculación entre las clases, lo que dificulta la modificación o expansión del código.
¿Qué representa la API y cómo está estructurada?
La API examinada tiene una estructura base con métodos GET
y ADD
. Para ejecutar estos métodos, utiliza StudentRepository
y Logbook
. Aquí se observa que, al buscar o añadir estudiantes, se registra un evento en Logbook
, lo que implica que el StudentController
depende fuertemente de estas clases.
¿Cuál es el problema con las pruebas unitarias?
Las pruebas unitarias actuales verifican métodos como get
sin evitar las interacciones con el sistema, como la escritura de datos en un archivo mediante Logbook
. Esto puede resultar problemático ya que las pruebas unitarias no deberían realizar operaciones de entrada/salida. Idealmente, estas pruebas deberían enfocarse solo en la lógica, sin efectos colaterales o dependencias externas.
¿Cómo se establece una solución utilizando la abstracción de interfaces?
Para solventar el problema de la dependencia directa, se introducen interfaces para cada clase dependiente. Crear una interfaz abstracta para StudentRepository
y Logbook
desacopla el controlador de sus implementaciones concretas. Así, el StudentController
no depende directamente de un tipo específico de StudentRepository
o Logbook
, sino de un contrato común definido por las interfaces.
Proceso para crear una interfaz
-
Crear la interfaz: Defina la interfaz
IStudentRepository
con los métodos requeridos:public interface IStudentRepository { IEnumerable<Student> GetAll(); void Add(Student student); }
-
Implementar la interfaz: Aplique esta interfaz a la clase concreta:
public class StudentRepository : IStudentRepository { public IEnumerable<Student> GetAll() { // Implementación... } public void Add(Student student) { // Implementación... } }
La misma lógica se aplica a Logbook
, creando una interfaz ILogbook
para encapsular su comportamiento.
¿Cómo se inyectan las dependencias?
La inyección de dependencias se realiza mediante constructor en el StudentController
. Donde antes se generaban objetos concretos con new
, ahora se espera recibir las interfaces en el constructor. Esto logra que el StudentController
actúe independiente de las implementaciones específicas.
public class StudentController {
private readonly IStudentRepository _studentRepository;
private readonly ILogbook _logbook;
public StudentController(IStudentRepository studentRepo, ILogbook log) {
_studentRepository = studentRepo;
_logbook = log;
}
// Métodos del controlador...
}
Configuración en .NET
Para optimizar la arquitectura y facilitar la configuración global de las dependencias se utiliza el contenedor de dependencias de .NET, donde se registran las interfaces y sus implementaciones concretas.
services.AddScoped<IStudentRepository, StudentRepository>();
services.AddScoped<ILogbook, Logbook>();
Esta configuración asegura que los servicios estén disponibles globalmente para cualquier clase que lo requiera.
¿Cómo se evalúa el impacto en las pruebas unitarias?
Gracias a la abstracción proporcionada por las interfaces, es posible introducir stubs o mocks para las pruebas unitarias, eliminando cualquier efecto colateral no deseado, como la creación de archivos o acceso a bases de datos.
Esto incrementa la robustez de las pruebas, permitiendo que se ejecuten en cualquier ambiente sin dependencias de sistemas externos o interacciones complejas. La inyección de dependencias hace que el StudentController
pueda ser probado con cualquier implementación que siga los contratos de las interfaces.
Implementar el principio de inversión de dependencias mediante interfaces y patrones de inyección no solo potencializa la flexibilidad del código sino también su capacidad de ser probado de manera uniforme y controlada. La meta es lograr un código que se adapta al cambio fácilmente, aislando y reutilizando componentes a través de contratos bien definidos.