Saltar al contenido

Artículos, tutoriales, trucos, curiosidades, reflexiones y links sobre programación web ASP.NET Core, MVC, Blazor, SignalR, Entity Framework, C#, Azure, Javascript... y lo que venga ;)

17 años online

el blog de José M. Aguilar

Inicio El autor Contactar

Artículos, tutoriales, trucos, curiosidades, reflexiones y links sobre programación web
ASP.NET Core, MVC, Blazor, SignalR, Entity Framework, C#, Azure, Javascript...

¡Microsoft MVP!
martes, 18 de diciembre de 2018
ASP.NET Core MVC Hace unos días publicaba un post sobre la mala idea que era tener controladores con demasiadas responsabilidades y cómo un constructor con demasiadas dependencias podía ser una señal (un 'code smell') de que las cosas no estaban yendo bien en ese sentido.

Por ejemplo, echando un vistazo al siguiente controlador podemos ver claramente una violación del Principio de Responsabilidad Única (SRP) en un controlador que conoce demasiados detalles sobre la forma de proceder al registrar un pedido:
public class OrderController: Controller
{
    private readonly IOrderServices _orderServices;
    [...] // Other private members

    public OrderController(IOrderServices orderServices, IUserServices userServices, 
        IMailingService mailingServices, ISmsServices smsServices, 
        IPdfGenerator pdfGenerator, IMapper mapper
    )
    {
        _orderServices = orderServices;       
        _userServices = userServices;
        [...] // Other assignments...
    }

    [HttpPost]
    public Task<IActionResult> Submit(OrderViewModel orderViewModel)
    {
        if(!ModelState.IsValid)
        {
            return View(orderViewModel);
        }
        var orderDto = _mapper.Map<OrderDto>(orderViewModel);
        var orderResult = await _orderServices.AddAsync(orderDto);
        if(!orderResult.Success)
        {
            return RedirecToAction("OrderError", new { error = orderResult.Error }));
        }
        var userPreferences = await _userServices.GetNotificationPreferencesAsync(CurrentUserId);
        var pdfUrl = await _pdfGenerator.GenerateOrderAsync(orderResult.Details);
        if(userPreferences.NotificationMode == NotificationMode.Sms)
        {
            await _smsServices.NotifyNewOrderAsync(orderResult.Details, pdfUrl);
        } 
        else 
        {
            await _mailingServices.NotifyNewOrderAsync(orderResult.Details);
        }
        return RedirectToAction("ThankYou", new { id = orderResult.Details.OrderId } );
    }
    ...
}
En dicho post comentaba también algunas cosas que podíamos hacer para solucionar el problema, así como una recomendación de lo que no debíamos hacer: disimular dependencias instanciando directamente componentes o utilizando otros "sabores" de inyección que hicieran menos evidente la relación de nuestra clase con otras de las que depende.

Pues bien, como hoy estamos algo rebeldes, vamos a ver las técnicas que nos permitirían hacerlo cuando sea necesario o, dicho de otra forma, qué alternativas tenemos para usar dependencias desde los controladores sin que estas sean suministradas mediante inyección en su constructor.
Precaución: estas técnicas son consideradas antipatrones o, como mínimo, code smells en la mayoría de los escenarios, así que usadlas poco, con precaución y siempre con conocimiento de causa ;)

Instanciación manual de componentes

En primer lugar, atentando directamente contra las buenas prácticas de diseño con bajo acoplamiento y la mismísima "D" de los principios SOLID, tenemos la posibilidad de instanciar los objetos que necesitemos, justo en el momento en que lo necesitemos.

El siguiente código muestra cómo podríamos reescribir el ejemplo anterior utilizando esta técnica:
public OrderController()
{
    // Nothing to do...
}

[HttpPost]
public Task<IActionResult> Submit(OrderViewModel orderViewModel)
{
    if(!ModelState.IsValid)
    {
        return View(orderViewModel);
    }

    var mapper = new Mapper();
    var orderDto = mapper.Map<OrderDto>(orderViewModel);

    using (var ctx = new ShoppingCartContext())
    {
        var orderServices = new OrderServices(ctx);
        var orderResult = await orderServices.AddAsync(orderDto);
        if(!orderResult.Success)
        {
            return RedirecToAction("OrderError", new { error = orderResult.Error }));
        }
        var userServices = new UserServices(ctx);
        var userPreferences = await userServices.GetNotificationPreferencesAsync(CurrentUserId);
        var prfGenerator = new PdfGenerator();
        var pdfUrl = await pdfGenerator.GenerateOrderAsync(orderResult.Details);
        if(userPreferences.NotificationMode == NotificationMode.Sms)
        {
            var smsServices = new SmsServices();
            await smsServices.NotifyNewOrderAsync(orderResult.Details, pdfUrl);
        } 
        else 
        {
            var mailingServices = new MailingServices();
            await mailingServices.NotifyNewOrderAsync(orderResult.Details);
        }
        return RedirectToAction("ThankYou", new { id = orderResult.Details.OrderId } );    
    }
}
Lo primero que habréis notado es que, obviamente, tendremos controladores con menos dependencias en el constructor, o incluso ninguna. También, las dependencias son creadas sólo cuando las necesitamos, por lo que se evitará la creación innecesaria de objetos que ocurre si nuestro constructor tiene muchos parámetros.

Sin embargo, hemos creado un monolito, una clase totalmente acoplada a sus dependencias. También hemos disminuido notablemente su legibilidad, al añadir mucha más complejidad al código. Ahora tenemos que encargarnos nosotros de gestionar instancias, sus liberaciones, suministrar las dependencias requeridas... todo un trabajazo que difícilmente será justificable teniendo otras opciones más sencillas como el uso de contenedores de inversión de control.

Y por último, no olvidemos que no podremos conocer las dependencias de un controlador a no ser que leamos detalladamente el código, puesto que éstas no estarán visibles en el constructor.

En general, por la gran cantidad de problemas que puede generar y el trabajo que supone, es una técnica que deberíamos usar rara vez en nuestros componentes para consumir dependencias.

Uso de Service locator

En segundo lugar, como una evolución de la anterior en un entorno guiado por la inyección de dependencias como es ASP.NET Core, podemos optar por el uso del patrón (y antipatrón al mismo tiempo) service locator.

Básicamente, esta técnica consigue desacoplar la clase de sus dependencias eliminando los new que la atan a implementaciones específicas mediante el uso de inversión de control. En lugar de instanciar nosotros las dependencias, usaremos un contenedor, en este caso el proveedor de servicios de .NET Core, para que se encargue de crear estos objetos y gestionar su ciclo de vida completo de forma automática.

Para ello, a nivel de constructor lo único que necesitaríamos en este caso es recibir una referencia hacia el proveedor de servicios que usaremos para obtener dependencias, como sigue:
public OrderController(IServiceProvider serviceProvider)
{
    _serviceProvider = serviceProvider;
}
Y hecho esto, en las acciones simplemente lo usaremos en los puntos donde necesitemos obtener una referencia a dependencias para utilizarlas desde nuestro código:
// Before:
...
using (var ctx = new ShoppingCartContext())
{
    var userServices = new UserServices(ctx);
    var userPreferences = await userServices.GetNotificationPreferencesAsync(CurrentUserId);
    ...
}

// Now:
...
var userServices = _serviceProvider.GetService<IUserServices>();
var userPreferences = await userServices.GetNotificationPreferencesAsync(CurrentUserId);
...
Recordad que para que esto sea posible debemos registrar y configurar apropiadamente los servicios en el contenedor de ASP.NET Core.
Como ventaja sobre la primera opción que hemos visto (instanciación manual), nuestro controlador estará desacoplado de sus dependencias y el código será más limpio porque todo el trabajo sucio de creación y liberación de objetos lo realizará el contenedor de servicios proporcionado por la infraestructura.

Eso sí, al igual que ocurría antes, seguiremos teniendo una clase cuyas dependencias no se manifiestan de forma explícita y clara, y para conocerlas habría que leer todo el código.

Asimismo, añadimos problemas derivados del uso de un service locator, como la posibilidad de que se produzcan errores en tiempo de ejecución, por ejemplo, si las dependencias requeridas en determinados escenarios no pueden ser satisfechas.

Inyección en parámetros de acciones

En tercer lugar, como una leve mejora respecto al punto anterior, es interesante saber que ASP.NET Core nos permite aplicar inyección de dependencias directamente como parámetros de las acciones.

Para ello, basta con decorar con [FromServices] los parámetros que deseamos inyectar desde el contenedor de servicios de ASP.NET Core, como en el siguiente ejemplo:
[HttpPost]
public Task<IActionResult> Submit(OrderViewModel orderViewModel, 
        [FromServices] IOrderServices orderServices,
        [FromServices] IUserServices userServices,
        [FromServices] IMapper mapper,
        ...
)
{
    if(!ModelState.IsValid)
    {
        return View(orderViewModel);
    }

    var orderDto = _mapper.Map<OrderDto>(orderViewModel);
    var orderResult = await orderServices.AddAsync(orderDto);
    if(!orderResult.Success)
    {
        return RedirecToAction("OrderError", new { error = orderResult.Error }));
    }
    var userPreferences = await userServices.GetNotificationPreferencesAsync(CurrentUserId);
    ...
}
Bien, aunque básicamente seguimos teniendo los mismos problemas que antes, es cierto que las dependencias quedan un poco más a la vista porque forman parte de la firma de las acciones.

Por último, es interesante destacar que, obviamente, [FromServices] se puede usar en acciones de controladores que también reciben dependencias en el constructor, lo cual puede sernos de utilidad cuando queramos hilar muy fino en términos de rendimiento. Por ejemplo, podríamos hacer que un controlador reciba en su constructor las dependencias que usaremos de forma general en todas sus acciones, y luego añadir con [FromServices] las dependencias exclusivas de cada acción:
public class OrderController: Controller
{
    ...
    public OrderController(IOrderServices orderServices, IUserServices userServices) 
    {  
        // We need these dependencies in all actions
        _orderServices = orderServices;
        _userServices = userServices;        
    }

    public Task<IActionResult> Submit([FromServices] IPaymentServices paymentServices) 
    {
        // We use payment services only when submitting an order
        ...
    }
}

En resumen

En este post hemos visto fórmulas para consumir dependencias desde un controlador sin necesidad de que éstas nos sean suministradas en su constructor: instanciación directa, service locator e inyección de dependencias en acciones.

Estas técnicas pueden ser interesantes en determinados escenarios, pero recordad que si el constructor de un controlador recibe más parámetros de la cuenta, probablemente se trata de un smell que nos está avisando de la ruptura del Principio de Responsabilidad Única y de que estamos creando una clase demasiado compleja. Y como vimos en su momento, la solución a este escenario es refactorizar.

Publicado en Variable not found.

Aún no hay comentarios, ¡sé el primero!