Constructores en los componentes

Los componentes pueden tener constructores con parámetros, es la forma habitual de asociar las dependencias (o colaboradores) en un componente. Pero, además de los colaboradores, puedes necesitar una cadena de conexión o cualquier otro valor que no sea una dependencia. Para configurar los parámetros del constructor de un componente, lo hacemos de la siguiente forma:

[Fact]
public void Resolver_componente_con_parametros()
{
    RegistrarContenedorUnoPorUno();

    IRepository repositorio1 = _container.Resolve<IRepository>(new { nombre = "Uno" });
    IRepository repositorio2 = _container.Resolve<IRepository>(new Arguments().Insert<string>("Dos"));

    Assert.Equal("Uno", repositorio1.NombreInterno);
    Assert.Equal("Dos", repositorio2.NombreInterno);
}

En este ejemplo, el componente que implementa la interfaz IRepository tiene en su constructor un parámetro de tipo string llamado “nombre“. A la hora de resolverlo, el valor de este parámetro es facilitado a partir del parámetro del método Resolve. Hay varias formas de pasar el parámetro. Para la variable repositorio1, generamos una clase anónima con una propiedad que debe llamarse igual que el parámetro del constructor (nombre en este caso). En cambio, para el repositorio2, sólo definimos que es un parámetro de tipo string y su valor. Internamente buscará el constructor con más parámetros que se ajuste a los argumentos facilitados. Si fueran dos los posibles constructores que pudieran usarse, Windsor no te asegura cuál es el constructor que usará.

Registrar varios componentes de un mismo servicio

¿Y si quiero tener el mismo servicio implementado por dos componentes diferentes? ¿o por el mismo componente pero con inicialización diferente? Para ello, deberás dar nombre a cada registro, fíjate:

[Fact]
public void Registrar_dos_componentes_del_mismo_tipo_con_inicializacion_y_nombre_diferente()
{
    _container.Register(
        Component
            .For<IRepository>()
            .ImplementedBy<ClientRepository>()
            .DependsOn(new { nombre = "Uno" }).Named("Uno"),
        Component
            .For<IRepository>()
            .ImplementedBy<ClientRepository>()
            .DependsOn(Property.ForKey<string>().Eq("Dos")).Named("Dos"),
        Component.For<IDao>().ImplementedBy<BasicDao>());

    IRepository repositorio1 = _container.Resolve<IRepository>("Uno");
    IRepository repositorio2 = _container.Resolve<IRepository>("Dos");

    Assert.Equal("Uno", repositorio1.NombreInterno);
    Assert.Equal("Dos", repositorio2.NombreInterno);
}

Registramos dos componentes del mismo servicio. Gracias al método Named les damos un nombre diferente a cada uno. A la hora de resolver el componente del servicio, simplemente le facilitamos el nombre de componente como primer parámetro. De esta forma, sabe a qué componente se refiere. Si no le facilitaríamos el nombre del componente, lo que nos devolvería sería el primer componente registrado de ese servicio.

Aquí también he añadido una novedad, el uso del método DependsOn. Lo que nos permite es pasar los valores de los parámetro del método Resolve como parámetros del constructor del componente. De esta forma, ya vienen predefinidos en el contenedor sin tener que facilitarlos en la llamada al método Resolve del contenedor.

Igual que antes, en el primer registro, uso una clase anónima y en el segundo registro uso la clase Property que nos permite sólo definir el tipo y valor del parámetro, sin tener que fijar el nombre.

Parámetros dinámicos en los constructores de los componentes

Castle Windsor nos da la posibilidad de definir parámetros para los constructores de los componentes que se “calculen” en el momento de crear la instancia, veamos un ejemplo:

[Fact]
public void Registrar_con_parametro_dinamico()
{
    DateTime fecha = DateTime.Now;
    _container.Register(
        Component
            .For<IRepository>()
            .ImplementedBy<ClientRepository>()
            .DynamicParameters((k, d) => d["Fecha"] = fecha));

    IRepository repositorio = _container.Resolve<IRepository>();

    Assert.Equal(fecha, repositorio.Fecha);
}

Usando el método DynamicParameters, le estamos definiendo un delegado con el código que deseamos que se ejecute en el momento de crear una instancia del componente registrado. En este caso, estamos asignando la fecha actual a la propiedad Fecha de ClientRepository.

Si tuviera un constructor con un parámetro llamado Fecha, lo hubiera usado, en vez de asignar valor a la propiedad. Es curioso este comportamiento y al principio puede causar desconcierto, pero es verdaderamente potente. En el siguiente punto conoceremos algo más de su potencia.

¿Dónde se definen las dependencias de mis componentes?

En ningún sitio, no es necesario. Windsor se encarga de detectarlas y asignarlas. Veamos la siguiente clase ClienteRepository usada en ejemplos anteriores:

public class ClientRepository : IRepository
{
    public string NombreInterno { get; private set; }
    public IDao Dao { get; set; }
    public DateTime Fecha { get; set; }

    public ClientRepository(string nombre, IDao dao)
    {
        Dao = dao;
        NombreInterno = nombre;
    }
}

Para poder instanciar un objeto de ClientRepository es necesario facilitar en su constructor un nombre y un objeto que implemente IDao. La configuración mínima y necesaria del registro del contenedor sería la siguiente:

_container.Register(
    Component
        .For<IRepository>()
        .ImplementedBy<ClientRepository>()
        .DependsOn(new {nombre = "Uno"}),
    Component.For<IDao>().ImplementedBy<BasicDao>());

Fíjate que, en DependsOn, no le estoy definiendo el valor del parámetro dao del constructor de ClientRepository. Esto es posible porque el contenedor tiene registrado un componente para el servicio IDao, por lo que no hace falta definirlo en DependsOn de forma explícita. Windsor se encarga implícitamente de inyectarle el valor al parámetro del tipo IDao.

Estarás pensando qué ocurre cuando, por ejemplo, tengo dos componentes diferentes para el mismo servicio IDao. ¿Cuál usará entonces? Usará el primero registrado. ¿Y si no quiero que use el primero registrado? Entonces, hay que especificárselo. Veamos cómo:

[Fact]
public void Deberia_instanciarse_el_repositorio_con_dao_extendido()
{
    _container.Register(
        Component.For<IDao>().ImplementedBy<BasicDao>().Named("basico"),
        Component.For<IDao>().ImplementedBy<ExtendedDao>().Named("extendido"),
        Component.For<IRepository>().ImplementedBy<ClientRepository>()
            .ServiceOverrides(ServiceOverride.ForKey<IDao>().Eq("extendido")));

    IRepository antonio = _container.Resolve<IRepository>();

    Assert.IsType(typeof(ExtendedDao), antonio.Dao);
}

Usando el método ServiceOverrides le especificamos el nombre del componente que queremos usar para el parámetro del constructor del ClientRepository del tipo IDao.

Handlers, selectores de componentes

Pongámonos en el siguiente escenario. Mis objetos de negocio usan el servicio IDao para almacenar los datos, pero dependiendo de la configuración quiero almacenar los datos en Sql Server o en Sql Lite. Para ello tengo dos componentes implementados. El BasicDao para SqlLite y el ExtendedDao para SqlServer. ¿Como configuro el contenedor para hacer esto posible? Una posible solución es la creación de un Handler que tome la decisión de qué componente IDao se usará en cada momento.

[Fact]
public void Deberia_resolver_dao_extendido_con_handler_configurado_para_dao_extendido()
{
    _container.Register(
        Component.For<IDao>().ImplementedBy<BasicDao>(),
        Component.For<IDao>().ImplementedBy<ExtendedDao>());
    _container.Kernel.AddHandlerSelector(new DaoHandlerSelector(TipoDao.Extendido));

    Assert.IsType(typeof(ExtendedDao), _container.Resolve<IDao>());
}

Los handlers afectan a todo el contenedor y se definen mediante el método AddHandlerSelector del objeto Kernel. Para simplificar el ejemplo, se facilita al DaoHandlerSelector el tipo de componente Dao que quiero que elija. Pero los handlers pueden tener toda la sofisticación que necesitemos. La implementación de la clase DaoHandlerSelector queda así:

public class DaoHandlerSelector : IHandlerSelector
{
    private TipoDao _tipoDao;

    public DaoHandlerSelector(TipoDao tipoDao)
    {
        _tipoDao = tipoDao;
    }

    public bool HasOpinionAbout(string key, Type service)
    {
        if (service == null) throw new ArgumentNullException("service");
        return service == typeof (IDao);
    }

    public IHandler SelectHandler(string key, Type service, IHandler[] handlers)
    {
        if (handlers == null) throw new ArgumentNullException("handlers");

        Type tipoAImplementar = GetTipoAImplementar();
        IEnumerable<IHandler> daoHandlers = handlers.Where(
            handler => handler.ComponentModel.Implementation == tipoAImplementar);

        if (!daoHandlers.Any())
        {
            throw new ApplicationException(
                string.Format(
                    "No se ha encontrado ningún componente del tipo {0}", tipoAImplementar.Name));
        }

        return daoHandlers.First();
    }

    private Type GetTipoAImplementar()
    {
        Type tipoAImplementar;
        switch(_tipoDao)
        {
            case TipoDao.Basico:
                tipoAImplementar = typeof (BasicDao);
                break;
            case TipoDao.Extendido:
                tipoAImplementar = typeof (ExtendedDao);
                break;
            default:
                throw new NotSupportedException("Tipo no reconocido");
        }
        return tipoAImplementar;
    }
}

Los handlers deben de implementar la interfaz IHandlerSelector y son llamados por el contenedor cuando él no es capaz de resolver por si sólo la dependencia que necesita para un componente.

El método HasOpinionAbout lo usa para saber si debe de consultar o no al Handler para una resolución concreta. El método SelectHandler, devuelve el IHandler del componente a usar el su resolución. Cada componente que registramos tiene un IHandler asociado. Cuando el contenedor intente resolver el servicio IDao que necesita y le pregunte al DaoHandlerSelector mediante el método SelectHandler, le facilitará todos los IHandlers de los componentes registrados para el servicio IDao.

Continúa con la 3ª parte de la introducción a Castle Windsor