Hay veces en las que una aplicacio tiene módulos que se pueden activar o desactivar, de manera que se habiliten o no ciertas características. Existen muchas formas de implementar esto, pero si estamos usando Castle Windsor como contenedor de inversión de control, hay una manera muy limpia de conseguirlo: utilizar handlers selectors.
Los handlers selectors constituyen un punto de extensibilidad de Windsor que nos permiten elegir qué implementación de un servicio concreto vamos a utilizar. Así, dependiendo de si un módulo está activo o no, podremos elegir si para los servicios que lo componen usamos una implementación real o una implementación vacía.
Para ver mejor esto, usaremos el siguiente ejemplo. Supongamos que tenemos una aplicación en la que se generan informes y se almacenan en disco. Además, en esa aplicación, es posible activar un módulo adicional que hace que esos informes se envíen por email a ciertos usuarios. Para activar este módulo, los usuarios de la aplicación tienen que haber adquirido una licencia especial que habilita esta funcionalidad (a veces hay que cobrar por las aplicaciones para comer…).
Una forma rápida de conseguir esto sería tener algo así:
public class ReportService { private readonly IEmailSender emailSender; private readonly ILicenseHolder licenseHolder; public ReportService(IEmailSender emailSender, ILicenseHolder licenseHolder) { this.emailSender = emailSender; this.licenseHolder = licenseHolder; } public void GenerateReports() { var report = ... // Lo que sea que haga falta para generar informes WriteToDisk(report); if (licenseHolder.IsEmailModuleActive) emailSender.Send(report); } }
¿Qué problemas tiene esta implementación?
Por una parte, hay un if
que queda un poco feo ahí en medio. Además, ReportService
adquiere la responsabilidad de coordinar si debe o no enviar los informes por email, lo que obliga a esa clase a saber que enviar o no emails es algo que depende de la licencia, y que por tanto necesita un ILicenseHolder
para saber qué hacer.
Handlers selectors al rescate
Usando un IHandlerSelector
podemos conseguir eso mismo pero de una forma mucho más limpia y fácil de mantener en el futuro. El interface IHandlerSelecter
tiene el siguiente aspecto:
public interface IHandlerSelector { bool HasOpinionAbout(string key, Type service); IHandler SelectHandler(string key, Type service, IHandler[] handlers); }
Como veis, se trata de un interface muy sencillo con sólo dos métodos. El primero, HasOpinionAbout
, permite al IHandlerSelector
indicar al kernel de Windsor si quiere participar o no en la selección del handler. El segundo, SelectHandler
, es el método que invocará el kernel de Windsor para que seleccionemos el handler que queramos.
La parte importante es SelectHandler
, donde deberemos hacer lo siguiente:
- Si queremos utilizar alguno de los handlers recibidos en el parámetro
handlers
, deberemos devolverlo. - Si no queremos intervenir en la decisión, deberemos devolver
null
, indicando así al kernel que siga invocando handlers selectors hasta que obtenga un handler o utilice el handler por defecto que considere oportuno.
A mi personalmente me parece un API un poco forzada. Si se puede devolver null
desde SelectHandler
para indicar que no queremos intervenir en la resolución, tal vez fuese posible evitar completamente el método HasOpinionAbout
. Además, en el método HasOpinionAbout
sólo se recibe el nombre con que se registró el servicio en el contenedor (key
) y el tipo de servicio (service
), lo que a veces puede no ser información suficiente para saber si queremos intervenir o no en la resolución.
Volviendo a nuestro ejemplo, lo que queremos es poder inyectar una implementación u otra de IEmailSender
dependiendo de si el usuario ha pagado por poder usar ese módulo o no. Para ello, crearemos dos implementaciones de IEmailSender
, una que envía emails y otra que no hace nada.
public interface IEmailSender { void Send(Report report); } public class EmailSender : IEmailSender { public void Send(Report report) { var smtpClient = new SmtpClient(...); // Enviar un email con el contenido del informe } } public class NullEmailSender : IEmailSender { public void Send(Report report) { // No hacemos nada... } }
Ahora que tenemos estas dos clases, vamos a crear un IHandlerSelector
que decida qué clase utilizar en función de lo que permita la licencia actual:
public class LicenseHandlerSelector : IHandlerSelector { private readonly ILicenseHolder licenseHolder; public LicenseHandlerSelector(ILicenseHolder licenseHolder) { this.licenseHolder = licenseHolder; } public bool HasOpinionAbout(string key, Type service) { return service == typeof(IEmailSender); } public IHandler SelectHandler(string key, Type service, IHandler[] handlers) { if (licenseHolder.IsEmailModuleActive) return handlers.First(h => h.ComponentModel.Implementation == typeof(EmailSender)); return handlers.First(h => h.ComponentModel.Implementation == typeof(NullEmailSender)); } }
Para indicar a Windsor que debe utilizar el handler selector que acabamos de crear, es necesario registrarlo durante la configuración del contenedor:
var container = new WindsorContainer(); var selector = new LicenseHandlerSelector(new LicenseHolder()); container.Kernel.AddHandlerSelector(selector); container.Register(Component.For<IEmailSender>().ImplementedBy<NullEmailSender>()); container.Register(Component.For<IEmailSender>().ImplementedBy<EmailSender>()); container.Register(Component.For<IReportService>().ImplementedBy<ReportService>());
Con esto, LicenseHandlerSelector
se encargará de elegir la implementación adecuada de IEmailSender
que recibirá ReportService
, por lo que el código de ReportService
se simplifica y ya no se responsabiliza de nada relacionado con licencias y módulos activados:
public class ReportService { private readonly IEmailSender emailSender; public ReportService(IEmailSender emailSender) { this.emailSender = emailSender; } public void GenerateReports() { var report = ... // Lo que sea que haga falta para generar informes WriteToDisk(report); // Ahora siempre se invoca emailSender.Send. Es resposabilidad de // otro (en este caso LicenseHandlerSelector) decidir si la implementación // de emailSender.Send efectivamente envía algo o no. emailSender.Send(report); } }
Resumiendo…
IHandlerSelector
es un mecanismo de extensión de Castle Windsor muy potente que nos permite seleccionar qué implementación vamos a utilizar de un servicio determinado. Gracias a esto, podemos evitarnos llenar la aplicación de if
s en sitios que no corresponde y limitar mejor las resposabilidades de cada clase.
El ejemplo de IHandlerSelector
que hemos visto es muy simple y adolece de un problema: falta de flexibilidad. Si mañana quiero añadir otro servicio dependiende de algún parámetro, tengo que tocar el IHandlerSelector
. Próximamente veremos como se puede evitar eso aplicando algunas convenciones a nuestro código.
Pingback: Usando convenciones para mejorar IHandlerSelector | Koalite's blog
Pingback: Extenders en KnockoutJS « Koalite's blog