Aplicación del Principio de Inversión de Dependencias en C#

Clase 14 de 16Curso 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

  1. Crear la interfaz: Defina la interfaz IStudentRepository con los métodos requeridos:

    public interface IStudentRepository {
        IEnumerable<Student> GetAll();
        void Add(Student student);
    }
    
  2. 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.