Aplicaciones modulares con Castle Windsor: IHandlerSelector

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 ifs 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.

2 comentarios en “Aplicaciones modulares con Castle Windsor: IHandlerSelector

  1. Pingback: Usando convenciones para mejorar IHandlerSelector | Koalite's blog

  2. Pingback: Extenders en KnockoutJS « Koalite's blog

Comentarios cerrados.