Uno de los patrones más utilizados en estos últimos tiempos es “Inyección de dependencia”. Vamos a ver algunos ejemplos, pero nos centraremos más que nada en conceptos de base, buenas prácticas y cómo usarlos correctamente de la mano de ASP.Net Core.
¿Qué es?
Comúnmente veremos que a la Inyección de dependencia se la llama DI, por sus siglas, este es un patrón de diseño que ayuda a crear aplicaciones débilmente acopladas. Este está basado en el principio, muy conocido, Inversion of Control (IoC) y en el principio de SOLID, la D, inversión de dependencia.
Básicamente lo que hacemos en IoC es invertir el control del flujo de la información dentro de nuestra aplicación. Bajo la premisa de que un objeto no debe crear otros objetos de los que dependa, lo que hacemos es inyectar el objeto construido que necesita desde algún servicio externo. En el caso de .Net Core, un contenedor de inyección.
Esto tiene muchas ventajas, entre ellas, permite desarrollar componentes desacoplados, simpleza en cambios futuros, excelente para la implementación de pruebas unitarias, factories para la creación de objetos y ayuda para reducir memory leaks.
En .Net Core
Muchos trabajamos con Asp.Net Core y sabemos que la inyección de dependencia la hacemos por el constructor, pero muy pocos saben que hay otros caminos para hacer esto:
La implementación de constructor
public class A { private B _dependency; public A(B instancedepency) { _dependency = instancedepency; } }
En un método
public class A { private B dependency; public void SetDependecy(B instancedepency) { this.dependency=instancedepency; } }
En una propiedad
public class A { public B dependency; }
¿Cuando puede ser útil en un método o en una propiedad? Supongamos que tenemos una clase que no podemos modificar el constructor por ninguna razón, entonces una buena opción es hacerlo por alguno de los otros 2 caminos.
Ejemplo
Ahora bien, vamos empezar por un ejemplo de como hacemos normalmente las cosas. Lo que haremos será crear un flujo normal sin la inyección de dependencia. Para esto crearemos primero una servicio de generador de personas por nombres y apellidos.
public class PeopleCreator { private readonly string[] _names = { "Luke", "John", "Grace", "Susan", "Doroty", "Duncan", "Homer", "Alisa" }; private readonly string[] _surnames = {"Smith", "Johnson", "Williams", "Miller", "Joones"}; public Person NewPerson(){ var rnd = new Random(); return new Person(){ Name = _names[rnd.Next(8)], Surname = _surnames[rnd.Next(5)], Age = rnd.Next(1, 120) }; } } public class Person{ public string Name { get; set; } public string Surname{get;set;} public int Age {get;set;} }
Como vemos en el ejemplo de código, tenemos una clase o servicio que creara personas aleatoriamente devolviendo un objeto del tipo Person. Ahora para consumir este servicio lo llamaremos desde una página web.
public IActionResult Index() { var peopleCreator = new PeopleCreator(); var newPerson = peopleCreator.CreatePerson(); return View(newPerson); }
En nuestra vista agregaremos que muestre los datos y nos dará como resultado lo siguiente:
Esta es la manera clásica como se hacen las cosas. Ahora bien, tenemos algunos problemas que resolver. Al tener la declaración del objeto en nuestro controlador, hemos roto la regla de “Single Responsibility Principle”, porque tenemos una clase creando objetos de otras clases, está haciendo más de una tarea. Otro punto es que nuestro código ha quedado acoplado, si necesitamos cambiar la clase PeopleGenerator por otro estamos obligados a cambiar nuestro controlador.
Por otro lado, hemos hecho que nuestro controlador sea mucho más difícil de probar. Cómo PeopleGenerator se crea dentro del método Index() no podremos hacer un fake de él en una prueba de unidad convirtiéndolas en poco fiables.
Para destacar, no menor, es que a lo largo de la vida de nuestra aplicación nuestro código se torna cada vez más sucio y difícil de comprender, mucho código redundante, y si necesitamos hacer algún cambio el impacto puede ser muy grande.
Por estas razones el mejor camino es utilizar Inyección de dependencia. Veamos como modificar nuestro código.
Inyección de dependencia
Lo primero que haremos será crear una interfaz IPeopleCreator. Podemos usar una de las herramientas de refactoring de Visual Studio presionando ctrl + . y seleccionamos la opción “Extraer Interface”.
public interface IPeopleCreator { Person CreatePerson(); } public class PeopleCreator : IPeopleCreator { .... }
Ahora que tenemos todo listo, modificaremos nuestro controlador para que acepte la inyección del objeto mediante el constructor:
public class HomeController : Controller { private readonly PeopleCreator _peopleCreator; public HomeController(PeopleCreator peopleCreator) { _peopleCreator = peopleCreator; } public IActionResult Index() { var newPerson = _peopleCreator.CreatePerson(); return View(newPerson); } }
Como vemos, creamos nuestra dependencia mediante la inyección en el constructor y se como buena práctica es asignada a una variable de solo lectura. Por último, directamente, desde la variable privada accedemos a él. Ahora si ejecutamos el código de esta manera veremos el siguiente error:
Esto sucede debido a que todavía no hemos registrado en nuestro contenedor de inyección el objeto que debe asignar, vamos a registrarlo. Esto debemos hacerlo desde el archivo Startup.cs en el método ConfigureServices:
public void ConfigureServices(IServiceCollection services) { services.AddScoped<IPeopleCreator, PeopleCreator>(); services.AddControllersWithViews(); }
De esta manera lo hemos inyectado en el contenedor. Fue agregado con AddScoped, ¿Qué es esto? tenemos 3 maneras de poder inyectar y con diferentes resultados correspondientes al ciclo de vida:
- Transient: Los objetos se crean cada vez que se solicitan al contenedor de servicios. Mejor para servicios ligeros y sin estado.
- Scoped: Los objetos una vez por solicitud del cliente(conexión). Útil cuando queremos usar la misma instancia dentro del mismo contexto de una petición HTTP.
- Singleton: El objeto la primera vez que se solicita o cuando Startup.ConfigureServices se ejecuta. Cada solicitud posterior utiliza la misma instancia. (cuidado con inyectar un DBContext de entity framework)
Ahora supongamos que necesitamos extender y creamos una nueva clase, por ejemplo SuperPeopleCreator.
public class SuperPeopleCreator : IPeopleCreator { ... }
Como la nueva clase implementa la interface IPeopleCreator no necesitaremos modificar nuestro controlador, solamente la configuración del contenedor:
public void ConfigureServices(IServiceCollection services) { services.AddScoped<IPeopleCreator, SuperPeopleCreator>(); services.AddControllersWithViews(); }
Hemos logrado varias mejoras:
- Código más limpio.
- Controlador desacoplado de la implementación del servicio.
- El controlador es más fácil de probar.
- Base más sólida para nuestra solución (usando abstracciones sobre clases concretas).
Conclusión
Hemos visto los conceptos esenciales de inyección de dependencia, inversión de control y principios de inversión de dependencia. Vimos cómo pasar de lo clásico al patrón de inyección y vimos cómo cambia nuestro flujo de aplicación. Con todos estos cambios nos brindan muchas mejoras, no cambiar servicios, evitar lógicas inconsistentes, código desacoplado y muy limpio. Espero que les sea de utilidad.
Referencias
- https://docs.microsoft.com/en-us/aspnet/core/mvc/controllers/dependency-injection
- https://docs.microsoft.com/en-us/aspnet/core/blazor/fundamentals/dependency-injection
- https://docs.microsoft.com/en-us/aspnet/core/fundamentals/dependency-injection
- https://docs.microsoft.com/en-us/aspnet/core/mvc/views/dependency-injection