Introducción con ejemplos a Castle Windsor, un contenedor de inversión de control para .NET (parte 2)
- Introducción con ejemplos a Castle Windsor (parte 1)
- Introducción con ejemplos a Castle Windsor (parte 3)
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
| Print article | This entry was posted by Rubén Bernárdez on 9 January, 2011 at 13:40, and is filed under .NET. Follow any responses to this post through RSS 2.0. You can leave a response or trackback from your own site. |


about 1 year ago
Buenas
Ya te ha salido un troll
, gracias a tu blog me ha dado envidia y por fin he publicado mi primer post [autobombo]http://www.dmarzo.com/2011/01/ef-carga-temprana-con-include-tipado.html[/autobombo].
El tema de los Handlers no lo acabo de ver muy útil (en un ejemplo sencillo es difícil ver las bondades de una idea), para el “trabajo” requiere.
Porque no usar un if de toda la vida:
if (tipoDao = TipoDao.Basico){ Component.For().ImplementedBy(),}
else if (tipoDao = TipoDao.ExtendedDao){ Component.For().ImplementedBy());}
else{ …}
Quizás usando handler se pueda cambiar el handler en tiempo de ejecución, aunque en este escenario faltan algunas piezas (habría que cambiar también la conexión a la base de datos, cerrar los daos que ya existan, etc..)
Saludos y enhorabuena por la serie sobre Castle.Windsor.
PD: Hammet uno de los lideres de la comunidad de Castle se paso al “lado del mal” http://hammett.castleproject.org/?p=312 en el verano de 2008 para trabajar en MEF no en Unity como te comente.
about 1 year ago
Hola David!
El IF de toda la vida puede ser una alternativa, pero en este caso le veo los siguientes inconvenientes:
- Si usara el contenedor directamente para instanciar el servicio IDao podría, pero al llamar directamente al contenedor y saltarme la lógica de los IF’s, obtendría una inconsistencia en mi software. Tenerlo implementado por un Handler, evita este problema.
- Pierdes toda la funcionalidad de los LifeStyle, tendrías que tenerlo en cuenta e implementártelo a mano. Nuevamente el contenedor quedaría inconsistente, ya que yo puedo configurar un LifeStyle en el servicio IDao que luego no se aplicará.
- El IF, ¿dónde lo metes? Tendrías que construirte tu clase factoría que internamente usara el contenedor e implementar en ella esta casuística en concreto. Windsor provee de una Facility llamada “Typed Factory Facility” (genera automáticamente tu clase factoría) que ya no podrías usar porque no deja implementar métodos específicos, sólo defines la interfaz. Más info en: http://stw.castleproject.org/Windsor.Typed-Factory-Facility.ashx
En resumen, usar los mecanismos de extensión de Windsor te permite implementar mi casuística concreta sin hacer el contenedor inconsistente y sin perder todas las características que incluye.
Saludos!