martes, 16 de julio de 2013
Sin duda, las convenciones están de moda. Cualquier framework actual que se precie trae su propio conjunto de convenciones, que no son sino una serie de reglas predefinidas, normalmente de carácter opcional, cuyo cumplimiento evitará que tengamos que tomar decisiones, evitará que cometamos errores, y, normalmente, aumentarán nuestra productividad.
Pues bien, comentaba hace poco que una de las sorpresas que acompañaba a la nueva versión de Unity, el Contenedor de Inversión de Control creado por el equipo de Patterns & Practices de Microsoft, es precisamente la posibilidad de usar convenciones a la hora de registrar asociaciones entre interfaces y clases de nuestro sistema, lo cual nos vendrá de fábula en muchas ocasiones.
Imaginad, por ejemplo, el siguiente código para Unity, muy habitual en sistemas que usan cualquier tipo de contenedor IoC para aplicar inyección de dependencias:
Pues bien, esto es un buen ejemplo de cómo las convenciones pueden ayudarnos bastante. Fijaos que el código anterior refleja intrínsecamente que existe una convención de nombrado de componentes en nuestro proyecto: todas las interfaces “ILoquesea” serán mapeadas a las clases “Loquesea” que lo implementen.
Usando Unity 3, todo ese código podría quedar de la siguiente forma:
Básicamente, el método
Publicado en: Variable not found.
Pues bien, comentaba hace poco que una de las sorpresas que acompañaba a la nueva versión de Unity, el Contenedor de Inversión de Control creado por el equipo de Patterns & Practices de Microsoft, es precisamente la posibilidad de usar convenciones a la hora de registrar asociaciones entre interfaces y clases de nuestro sistema, lo cual nos vendrá de fábula en muchas ocasiones.
Imaginad, por ejemplo, el siguiente código para Unity, muy habitual en sistemas que usan cualquier tipo de contenedor IoC para aplicar inyección de dependencias:
container.RegisterType<IProductServices, ProductServices>( new PerRequestLifetimeManager()); container.RegisterType<IInvoiceServices, InvoiceServices>( new PerRequestLifetimeManager()); container.RegisterType<ICustomerServices, CustomerServices> (new PerRequestLifetimeManager()); container.RegisterType<IProductRepository, ProductRepository>( new PerRequestLifetimeManager()); container.RegisterType<IInvoiceRepository, InvoiceRepository>( new PerRequestLifetimeManager()); container.RegisterType<ICustomerRepository, CustomerRepository>( new PerRequestLifetimeManager()); container.RegisterType<IUnitOfWork, UnitOfWork>( new PerRequestLifetimeManager()); container.RegisterType<ILogger, Logger>( new PerRequestLifetimeManager()); container.RegisterType<IFileStorage, FileStorage>( new PerRequestLifetimeManager()); // etc...Como habréis observado, en este caso el registro de interfaces y clases tiene un patrón común: simplemente mapeamos los interfaces con nombre
ISomething
a su implementación concreta en la clase Something
, una y otra vez, para todas las dependencias de nuestro sistema. Esto, además de ser bastante tedioso y repetitivo, es muy propenso a fallos, pues es normal olvidarse de registrar nuevas clases e interfaces conforme se van incluyendo en el proyecto.Pues bien, esto es un buen ejemplo de cómo las convenciones pueden ayudarnos bastante. Fijaos que el código anterior refleja intrínsecamente que existe una convención de nombrado de componentes en nuestro proyecto: todas las interfaces “ILoquesea” serán mapeadas a las clases “Loquesea” que lo implementen.
Usando Unity 3, todo ese código podría quedar de la siguiente forma:
container.RegisterTypes( AllClasses.FromAssemblies(typeof(MvcApplication).Assembly), WithMappings.FromMatchingInterface, WithName.Default, WithLifetime.Custom<PerRequestLifetimeManager> );Y lo mejor es que no necesitaremos venir al registro a introducir las nuevas interfaces y clases que vayamos añadiendo a nuestro proyecto: si siguen la convención de nombrado serán registradas automáticamente.
Básicamente, el método
RegisterTypes()
acepta los siguientes parámetros:- una colección de los tipos de datos a mapear, en forma de
IEnumerable<Type>
. En el ejemplo anterior, usamos la claseAllClasses
, provista también por Unity, para obtener los tipos cargados en el ensamblado de la aplicación mediante su métodoFromAssemblies()
, pero, por supuesto, podríamos incluir aquí otros ensamblados separándolos por comas, o incluso indicar que queremos escanear todos los ensamblados cargados usando el métodoFromLoadedAssemblies().
- una función lambda o delegado que recibirá cada uno de los tipos anteriores y retornará los interfaces a asociar a dicho tipo en el registro de Unity. La clase estática
WithMappings
proporciona varios métodos que retornan este delegado “precocinado”: FromMatchingInterface,
el usado en el ejemplo anterior, que retorna todos los interfaces implementados por el tipo suministrado, y cuyo nombre corresponde al patrón “I”+nombre de la clase.FromAllInterfaces
, que retornará todos los interfaces que implementen el tipo suministrado.FromAllInterfacesInSameAssembly
, que, como su nombre indica, es idéntico al anterior, pero referido exclusivamente a los interfaces definidos en el mismo ensamblado que la clase que lo implementa.
- una lambda que permite especificar el nombre con el que será almacenada la asociación entre interfaz y clase en el registro de Unity. En el ejemplo podemos ver que la clase
WithName
ofrece algunos métodos válidos, comoDefault
(que establece anull
el nombre), oTypeName
, que lo obtendrá del nombre del tipo del componente. En la mayoría de los casos, pasarnull
oWithName.Default
bastará, pues este nombre se usa exclusivamente cuando se desea resolver un tipo haciendo referencia expresa a su asociación en el registro, lo cual no es muy frecuente.
- otra lambda, que será invocada para cada uno de los tipos de datos pasados en el primer parámetro y que retornará el
LifetimeManager
apropiado para cada uno. La clase estáticaWithLifeTime
ofrece varios métodos de utilidad ya preparados, comoHierarchical
,PerThread
,Transient
, oCustom<T>
, siendoT
elLifetimeManager
que nos interese. En este caso, observad que otorgábamos a todos los objetos creados desde el contenedor una vida “PerRequest”, es decir, que serán liberados cuando termine el proceso de la petición, que es lo habitual en desarrollos para la web.
MyProject.Application
de cualquier ensamblado, podríamos hacerlo así, retocando el primer parámetro:container.RegisterTypes( AllClasses.FromLoadedAssemblies() .Where(a=>a.Namespace == "MyProject.Application"), WithMappings.FromMatchingInterface, WithName.Default, WithLifetime.Custom<PerRequestLifetimeManager> );O, por ejemplo, si quisiéramos usar un LifetimeManager per request sólo para las clases pertenecientes a dicho espacio de nombres:
container.RegisterTypes( AllClasses.FromAssemblies(typeof (MvcApplication).Assembly), WithMappings.FromMatchingInterface, WithName.Default, type => type.Namespace == "MyProject.Application" ? (LifetimeManager)new PerRequestLifetimeManager() : (LifetimeManager)null );O, algo simplificado, si quisiéramos cambiar la convención de nombrado de interfaces de
IComponent
a ComponentInterface
, podríamos reflejarlo así en el momento realizar el registro:container.RegisterTypes( AllClasses.FromAssemblies(typeof(MvcApplication).Assembly), type => type.GetInterfaces().Where(i => i.Name == type.Name + "Interface"), WithName.Default, WithLifetime.Custom<PerRequestLifetimeManager> );En definitiva, el uso de convenciones en el registro de componentes de nuestro contenedor IoC es un avance que ya teníamos disponible en otros paquetes, pero que Unity acaba de incorporar en su versión 3.0. Sin duda, una gran ayuda para reducir la base de código, evitar errores y ser más productivos.
Publicado en: Variable not found.
8 Comentarios:
Hola Jose,
Genial el post, la verdad es que esto ahorrará mucho código y parece muy flexible.
Te animo a que sigas escribiendo cosas de Unity, estoy con él y me viene de perlas ;-)
Un saludo.
Buenas.
Estoy desarrollando una aplicación para Windows 8, y estoy usando Unity,
¿puedo usar las convenciones?
La verdad es que me quitaría mucho trabajo.
Un saludo, y gracias.
Hola!
Muchas gracias a ambos por comentar :)
@Sergio, algo más tengo pendiente, no acaba aquí la cosa con Unity ;)
@Amoedo, según leo, funcionando con apps Windows Store, aunque con algunas limitaciones (http://blogs.msdn.com/b/agile/archive/2013/03/12/unity-configuration-registration-by-convention.aspx).
Saludos.
Hola Jose,
Hoy he vuelto a tu post (una vez más) porque he cambiado de AutoFac a Unity en el proyecto MVC 4 que estoy desarrollando (a priori, me entero más con Unity y parece está más arraigado en el mundo .net).
Todo ha ido bien, pero me ha pasado una cosa "extraña" que no sé razonar.
Me ha dado por depurar la colección Registrations para ver que estaba todo OK, que sólo se registraba lo que yo quería, pero... sorpresa... estas 2 llamadas no registran lo mismo:
container.RegisterTypes(types, WithMappings.FromMatchingInterface);
container.RegisterTypes(types, WithMappings.FromMatchingInterface, WithName.Default, WithLifetime.Transient);
La primera hace su trabajo, busca interfaces con el mismo nombre que clases concretas y las registra con el lifetime Transient (el que utiliza Unity por defecto).
Sin embargo la segunda registra tanto la coincidencia con la interfaz como la misma clase ¿eihnn? :) Es decir, si tengo OrdersService e IOrderService hace 2 registros. El primero desde IOrdersService a OrdersService y el segundo desde OrdersService a OrdersService... y digo yo ¿Para qué y por qué quiero este segundo registro?
De hecho, este "extraño" comportamiento lo hace con WithLifetime.Transient, también con WithLifetime.None... no sé, no me entero. La única solución que he encontrado es pasar de utilizar la sobrecarga de RegisterTypes que no utiliza el parámetro getLifeTimeManager (pero claro, entonces las convenciones pierden algo de fuelle).
Lógicamente, aunque Unity registre x2 (porque él lo vale), la aplicación sigue funcionando sin problemas porque esos "registros fantasmas" no se utilizan, pero me gustaría registrar "sólo" lo que realmente quiero inyectar.
He googleado un poco y no he visto nada, si es una "duda muy peluda" o el post no el sitio, me lo dices e intento resolverla por otro lado :)
Gracias!
Hola,
bueno, creo que puede tener su sentido que cada clase esté registrada de forma independiente.
Hay veces que no hay interfaz para una clase pero quieres utilizar Unity como Service Locator, o para generar singletons, o para gestionar la vida del objeto.
Entiendo que ese doble registro es el que permite que puedas pedir a Unity una instancia de la clase "X" directamente, que él resuelva sus dependencias (quizás "X" requiera instancias de "Y" y de "Z" en su constructor), y, por ejemplo en el mundo web, que cuando acabe la petición se lo cepille de forma automática :)
Saludos.
Saludos!
Gracias Jose por tu respuesta.
Yo la verdad es que sigo mosca con el tema sobre todo porque estoy usando FromMatchingInterface. Es decir, según convención le estoy diciendo que sólo quiero coincidencias por Servicio e IServicio.
Entiendo lo de registrar clases concretas, sobre todo después de leerme el tocho de Mark Sheemman :-) pero que si le pasas unos parámetros haga una cosa y sino se los pasas haga otra... pues me ha dejado frío.
Mil gracias por contestar
Un saludo!
Hola Jose,
Saludos desde Santa Cruz, Bolivia. Agradecerte por el tiempo que le dedicas, desde aquí siempre te sigo.
Tengo una duda al respecto, si quisiera inyectar las interfaces haciendo uso de las convenciones, pero tengo algunas clases en la que tengo que inyectar el constructor como seria?
Saludos.
Hola, Luis!
Muchas gracias por tus comentarios :)
> tengo algunas clases en las que tengo que inyectar el constructor
No he entendido exactamente a qué te refieres... las convenciones las usas para simplificar el registro de servicios abstractos con servicios específicos (interfaces con implementaciones), pues evita tener que hacerlos uno a uno.
Pero si en algún caso necesitas hacer un registro más específico, por ejemplo usando un constructor concreto o inyectando parámetros, puedes hacerlo como siempre, usando clases como InjectionConstructor o InjectionMethod (https://msdn.microsoft.com/en-us/library/ff660882(v=pandp.20).aspx)
Saludos!
Enviar un nuevo comentario