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, 27 de noviembre de 2018
Desarrolladores Como sabemos, la inyección de dependencias está grabada a fuego en los genes de ASP.NET Core. La mayoría de sus componentes la usan internamente para obtener acceso a otros componentes que necesitan para cumplir sus funciones y, además, es una práctica recomendada en la parte que nos toca a nosotros como desarrolladores de aplicaciones.

Por ello, en ASP.NET Core MVC, lo habitual es que implementemos nuestros controladores atendiendo a este principio, y para ello utilicemos la técnica de inyección de dependencias en el constructor:
public class InvoiceController: Controller
{
    private readonly IInvoiceServices _invoiceServices;
    private readonly IMapper _mapper;
    private readonly ILogger<InvoiceController> _logger;

    public InvoiceController(
        IInvoiceServices invoiceServices, 
        ILogger<InvoiceController> logger,
        IMapper mapper)
    {
        _invoiceServices = invoiceServices;
        _logger = logger;
        _mapper = mapper;
    }
    ...
}
Nota: aunque en este post estemos hablando de controladores ASP.NET Core MVC, las ideas que vamos a comentar aquí son también aplicables a ASP.NET MVC "clásico" e incluso a otro tipo de frameworks que comparten los mismos conceptos.

Controladores endiosados, o dioses controladores

Cuando el controlador crece, es probable que la lista de parámetros del constructor también aumente de forma significativa, lo cual se convierte automáticamente en un code smell (¡hola, "S" de SOLID!). Llegado este caso, probablemente estemos otorgando demasiadas responsabilidades a este controlador, y, con ello, disminuyendo su cohesión, legibilidad, mantenibilidad y robustez.

De la misma forma, podemos encontrarnos con un exceso de responsabilidades, pero no sólo desde el punto de vista numérico, sino conceptual. A veces trasladamos al controlador responsabilidades que serían más propias del modelo, y esto se refleja claramente en las dependencias que declaramos en su constructor.

Por último, también hay que pensar que no todas las acciones del controlador utilizarán todas las dependencias recibidas. Por tanto, en determinadas ocasiones estaremos instanciando un grafo de objetos excesivo para el uso real que vamos a darle en el proceso de una petición, lo cual puede tener su repercusión en términos de rendimiento y memoria, sobre todo si la estructura de dependencias es compleja. Esto podría ser especialmente doloroso si alguna de las dependencias tiene un constructor complejo o que realice tareas costosas (algo que, por otra parte, no es recomendable en absoluto), porque estaríamos sometiendo al sistema a un stress innecesario cuando la acción a ejecutar no requiera dicha dependencia para llevar a cabo su misión.

La cuestión es que, con el paso del tiempo, si no tenemos cuidado podríamos llegar a tener un controlador con aspiraciones divinas como el siguiente:
public class InvoiceController: Controller
{
    private readonly IInvoiceServices _invoiceServices;
    [...] // Other private members

    public InvoiceController(
        IInvoiceServices invoiceServices, 
        ICustomerServices customerServices,
        ISupplierServices supplierServices,
        IStockServices stockServices,
        IDeliveryServices deliveryServices,
        IDigitalSignatureServices digitalSignatureServices,
        IAuthorizationServices authorizationServices, 
        IAuditServices auditServices, 
        IMailingService mailingServices, 
        ISmsServices smsServices, 
        IPushServices pushServices, 
        IPdfGenerator pdfGenerator,
        IMapper mapper,
        ILogger<InvoiceController> logger,
        [...]
    )
    {
        _invoiceServices = invoiceServices;       
        _customerServices = customerServices;
        _supplierServices = supplierServices;
        _stockServices = stockServices;
        _deliveryServices = deliveryServices;
        _digitalSignatureServices = digitalSignatureServices;
        _authorizationServices = authorizationServices;
        _auditServices = auditServices;
        _mailingServices = mailingServices;
        _smsServices = smsServices;
        _pushServices = pushServices;
        _pdfGenerator = pdfGenerator;
        _mapper = mapper;
        [...]
    }
    ...
}

Pues sí, así son mis controladores... ¿cómo lo soluciono?

Si ya estamos en este punto, o incluso bastante antes, podemos asegurar que tenemos un problema. Lo mejor es empezar a actuar inmediatamente; cuanto más tardemos en empezar a refactorizar, más difícil será el proceso para dejarlo todo en condiciones.

Esta refactorización podría ir enfocada a las siguientes líneas de actuación:
  • Trocear el controlador en controladores más especializados, pequeños y manejables. Por ejemplo, un controlador para gestionar todo lo relativo con facturación suena demasiado ostentoso, ¿verdad? Quizás deberíamos plantearnos tener controladores específicos para la generación de facturas, otros para reportes, otro para firmas electrónicas, etc. El resultado será un diseño respetuoso con el principio SRP y clases bastante más especializadas y con muchas menos dependencias.

  • Extraer funcionalidades trasversales a filtros. Por ejemplo, en el caso anterior serían buenos candidatos los servicios de autorización o auditoría. Esto nos ayudará a tener controladores mucho más pequeños y a reutilizar bastante código, pues en muchas ocasiones estas operaciones trasversales son comunes entre acciones e incluso entre distintos controladores.

  • Mover funcionalidades al Modelo. En el ejemplo anterior lo vemos claramente: si nuestra aplicación debe enviar una notificación cada vez que se crea una factura, el envío del mail no debería realizarlo el controlador, sino la clase del Modelo que proporciona los servicios de creación de las mismas, porque este proceso forma parte de la lógica de negocio del sistema. Confundir la ubicación de determinadas funciones es uno de los principales causantes de estos controladores subidos de peso.

  • Agrupar servicios en abstracciones de nivel superior. Por ejemplo, no tiene sentido que el controlador reciba dependencias a servicios de notificaciones por email, SMS o push; quizás deberíamos plantearnos tener un INotificationSender que sea el que decida por qué vía debe notificar al usuario y encapsule las dependencias que implementan cada tipo de mensaje. En muchas ocasiones, el uso de niveles de abstracción incorrectos hacen que nuestras clases tengan que recibir más dependencias de la cuenta y aumentar la cantidad de código implementado en las mismas.

  • No estaría de más echar un vistazo a patrones como Mediator, Facade, Command, y arquitecturas tipo CQS, uso de eventos u otras técnicas que nos ayuden a simplificar nuestros componentes y las relaciones entre ellos.
En cualquier caso, salvo ocasiones muy justificadas, lo que no deberíamos hacer es ceder ante la tentación de intentar disimular el exceso de dependencias mediante la instanciación directa o el uso de service locators u otras técnicas de inyección. Aunque estas herramientas pueden resultar útiles en determinados escenarios, pueden dar lugar a problemas porque harán menos visibles las dependencias y, por tanto, menos evidentes la violación del principio de separación de responsabilidades, que al final es lo que tenemos que evitar.

Pero... ¿cuántos parámetros son demasiados parámetros en el constructor?

Pues no creo que haya un número exacto de parámetros que actúe como frontera entre lo correcto y lo incorrecto. Como suele ocurrir, depende de los escenarios: puede haber controladores en los que tres parámetros ya sean demasiado porque estemos introduciendo en él más responsabilidades de la cuenta, y puede haber otros donde recibir seis dependencias pueda resultar correcto porque sean esenciales para realizar las tareas que tenga encomendadas.

Eso sí, parece existir un cierto consenso en que a partir de cuatro parámetros debemos activar las alertas y empezar a plantearnos si es necesario refactorizar, y el nivel de probabilidad de que sea necesaria dicha refactorización irá subiendo de forma exponencial conforme aumente el número de parámetros.

Publicado en: www.variablenotfound.com.

2 Comentarios:

Jonatan Rodríguez Suárez dijo...

También existe la posibilidad de inyectar dependencias en las acciones del controlador, de esta forma si una acción determinada es la única que necesita determinada dependencia no es necesario cargarla en el constructor, repercutiendo en el resto de acciones del controlador. Simplemente hay que decorar la dependencia con el atributo [FromServices]

https://docs.microsoft.com/es-es/aspnet/core/mvc/controllers/dependency-injection?view=aspnetcore-2.1#action-injection-with-fromservices

José María Aguilar dijo...

Efectivamente, Jonathan! De hecho, mi siguiente post va sobre este atributo y alguna otra forma de eliminar las dependencias en el constructor :)

Muchas gracias por comentar!