Dentro de Microsoft patterns & practices
Inserción de dependencia en bibliotecas
Refactorización de Microsoft Enterprise Library
La Inserción de dependencia (DI) es un patrón que ha ido ganado tracción en la comunidad de desarrolladores de .NET en los últimos años. Algunos blogueros importantes han hablado por largo tiempo acerca de los beneficios de DI. Se han publicado muchos artículos sobre el tema en MSDN Magazine. .NET 4.0 incluirá cierta funcionalidad similar a DI, con planes de hacerla crecer hasta convertirse en un sistema de DI completo en el futuro.
Al leer las publicaciones de blogs y artículos sobre DI, observé una pequeña pero importante tendencia en la cobertura del tema. Los autores hablan acerca del uso de DI en el contexto de una aplicación completa. Pero... ¿qué pasa si se desea escribir una biblioteca o un marco que usa DI? ¿Cómo afecta este cambio en el enfoque al uso del patrón? Esto es algo que nosotros (el equipo de Enterprise Library de patterns & practices) enfrentamos de lleno hace algunos meses cuando trabajábamos en la arquitectura de Enterprise Library 5.0.
Información previa
Microsoft Enterprise Library (Entlib) es un lanzamiento muy conocido del grupo Microsoft patterns & practices. Con más de 2 millones de descargas hasta la fecha, se usa casi en todos los nichos imaginables, desde instituciones financieras y gubernamentales hasta restaurantes y fabricantes de equipos médicos. Entlib es, tal como el nombre lo indica, una biblioteca que ayuda a un desarrollador a afrontar las inquietudes en común que comparten también muchos desarrolladores empresariales. Si no está familiarizado con Entlib, dé un vistazo a nuestro sitio en el Centro de desarrollo de patterns & practices para obtener más información.
Entlib es controlada en gran medida por la configuración. Una gran parte de su código está dedicada a la lectura de la configuración y al ensamblado de gráficos de objeto según esa configuración. Los objetos de Entlib pueden llegar a ser muy complejos. La mayoría de los bloques contienen gran cantidad de funcionalidad opcional. Además, también hay una gran cantidad de infraestructura subyacente para admitir, por ejemplo, la instrumentación que también necesita conectarse. Como no queríamos que nuestros usuarios tuvieran que crear manualmente proveedores de instrumentación, leer la configuración, etc., sólo para usar Entlib, la creación de objetos se encapsuló detrás de objetos de fábrica y fachadas estáticas.
La parte fundamental de Entlib en la versión 2 a la versión 4 es un marco llamado ObjectBuilder. Sus autores describen a ObjectBuilder como “un marco para la creación de contenedores de inserción de dependencia”. Enterprise Library es sólo uno de los proyectos de patterns & practices que usa ObjectBuilder; otros incluyen Composite UI Application Block, Smart Client Software Factory y Web Client Software Factory. En especial, Entlib tomó la parte de “marco” de la descripción y construyó un gran conjunto de personalizaciones para ObjectBuilder. Estas personalizaciones proporcionaron la funcionalidad necesaria para leer la configuración de Entlib y ensamblar gráficos de objeto. En muchos casos, también eran necesarias para mejorar el rendimiento de la implementación de ObjectBuilder de acciones.
La desventaja fue que demoró un buen tiempo comprender el ObjectBuilder (un diseño extremadamente abstracto, además de la ausencia total de documentación, hizo que el ObjectBuilder se ganara una merecida reputación de ser complejo) y las personalizaciones de Entlib. Como resultado, las personas que querían escribir bloques personalizados que se unieran a la estrategia de creación de objetos de Entlib a menudo se enfrentaban al obstáculo que significaba la enorme curva de aprendizaje que debían superar sólo para empezar.
Y como si las complicaciones no fueran suficientes, en Entlib 4.0 lanzamos el contenedor de inserción de dependencia Unity. Con las diversas ventajas de DI, queríamos asegurarnos de que los clientes que, por cualquier razón, no podían usar uno de los muchos buenos contenedores de código abierto tuvieran una buena opción para obtener DI de Microsoft. Y, por supuesto, quisimos facilitar el lograr que los objetos de Entlib funcionaran también al usar Unity. En Entlib 4.0, la integración de Unity terminó siendo un sistema de creación de objetos paralelo junto a la infraestructura existente de ObjectBuilder. Ahora los escritores de bloques debían conocer no sólo las extensiones de Entlib y ObjectBuilder, sino que también los elementos internos de Unity, además de algunas extensiones de Entlib. Ni un paso en la dirección correcta.
Avance hacia la simplicidad
Comenzamos a trabajar en Entlib 5.0 en abril del año 2009. Un tema importante para esta versión fue “simplicidad para ganar”. Esto incluía no sólo simplicidad para el usuario final (los desarrolladores que llamaban a Entlib), sino en el mismo código de Entlib. Las mejoras aquí nos facilitarían mantener avanzando a Entlib y facilitarían también la comprensión, personalización y ampliación para nuestros clientes.
Una de las áreas importantes en las que sabíamos que tendríamos que trabajar era la estructura de creación de objetos... ¿o debería decir “estructuras”? Mantener dos conjuntos de código paralelos pero diferentes para la misma funcionalidad es una receta segura para el desastre. Había que hacer algo.
Definimos estos objetivos para la refactorización:
- El código cliente existente no debiera cambiar sólo porque cambiaba la arquitectura. Está bien que sea necesario volver a compilar, pero no está bien solicitar cambios en el código fuente (por supuesto, la API cliente podría cambiar por otras razones). Las API internas o de extensibilidad son parte del juego.
- Quitar las estructuras redundantes de la creación de objetos. Sólo deberíamos tener una manera de crear objetos, no dos (o más).
- Los clientes a los que no les interese DI no deberían verse afectados por Entlib al usarla de manera interna.
- Los clientes a los que sí les interesa DI pueden elegir el contenedor que desean usar y de donde poder extraer sus objetos y los objetos de Entlib.
Estos objetivos tenían algunas implicaciones, tanto por separado como en combinación. El objetivo de “una estructura de creación de objetos” era, al menos en la superficie, muy sencillo. Decidimos quitar completamente el sistema basado en ObjectBuilder y usar un contenedor de DI como nuestro motor de creación de objetos de manera interna. Pero ahí llegamos a la parte de “el código cliente existente no debiera cambiar”. La API de Entlib clásica es un conjunto de fachadas y fábricas estáticas. Por ejemplo, para registrar un mensaje con el bloque de registro se hace lo siguiente:
Logger.Write("My Message");
En realidad, la fachada del registrador usa una instancia de un objeto LogWriter para realizar el trabajo real. Entonces... ¿cómo la fachada del registrador obtiene el LogWriter? LogWriter es una clase muy compleja con gran cantidad de dependencias, por lo que no es posible simplemente iniciarla y esperar que la configuración se conecte de manera adecuada. Llegamos a la conclusión de que necesitábamos una instancia de contenedor global para el registrador y todas las demás clases estáticas de la API. Pudimos mantener sólo un contenedor Unity global, pero luego nos encontramos con la parte de “los clientes pueden elegir el contenedor que desean”.
Queremos que la combinación de Unity y Entlib sea una experiencia de primera clase. También queremos ofrecer esa experiencia de primera clase con otros contenedores. Mientras que las capacidades generales de los contenedores de DI son comunes en todos los aspectos, las formas en que obtiene acceso a esas capacidades varían ampliamente. De hecho, muchos creadores de contenedores consideran que sus API de configuración son su principal ventaja competitiva. Entonces... ¿cómo podemos asignar nuestra configuración de Entlib a API de contenedor tremendamente diferentes?
La solución de informática clásica
Es un lugar común en la informática: es posible solucionar cada problema informático si se agrega una capa de direccionamiento indirecto. Y precisamente así solucionamos nuestro problema de independencia del contenedor. La capa de direccionamiento indirecto es lo que llamamos un configurador de contenedor. En su núcleo, la función del configurador es leer la configuración de Entlib y configurar un contenedor que coincida.
Desafortunadamente, leer la configuración no es suficiente. El formato de archivo de la configuración de Entlib está muy centrado en el usuario final. Usted configura categorías de registro, directivas de excepción y almacenes de copias de seguridad en memoria caché. No dice nada acerca de los objetos que realmente eran necesarios para implementar esa funcionalidad, ni de los valores que se debían transmitir a los constructores ni de las propiedades que se debían definir. Por otro lado, la configuración del contenedor de DI trata de “asignar esta interfaz a este tipo”, “llamar a este constructor” y “definir esta propiedad”. Era necesario agregar otra capa de direccionamiento indirecto que asignara la configuración de un bloque a los objetos requeridos reales para implementar ese bloque. La alternativa era hacer que cada configurador (era necesario un configurador por contenedor) conociera los detalles de cada uno de los bloques. Esto es inmediatamente insostenible: cada cambio del código de un bloque puede generar una onda expansiva a lo largo de todos los configuradores. ¿Y qué pasa cuando alguien escribe un bloque personalizado?
Terminamos con un conjunto de objetos que llamamos TypeRegistrations. Las diversas secciones de configuración son responsables de generar un modelo de registro de tipos; una secuencia de objetos de TypeRegistration. La interfaz de TypeRegistration aparece en la figura 1.
Figura 1 Clase de TypeRegistration
public class TypeRegistration
{
public TypeRegistration(LambdaExpression expression);
public TypeRegistration(LambdaExpression expression, Type serviceType);
public Type ImplementationType { get; }
public NewExpression NewExpressionBody { get; }
public Type ServiceType { get; private set; }
public string Name { get; set; }
public static string DefaultName(Type serviceType);
public static string DefaultName<TServiceType>();
public LambdaExpression LambdaExpression { get; private set; }
public bool IsDefault { get; set; }
public TypeRegistrationLifetime Lifetime { get; set; }
public IEnumerable<ParameterValue> ConstructorParameters { get; }
public IEnumerable<InjectedProperty> InjectedProperties { get; }
}
Se ven muchos elementos aquí, pero la estructura básica es muy simple. Esta clase describe la configuración requerida para un solo tipo. ServiceType es la interfaz que el usuario solicitará al contenedor, mientras que ImplementationType es el tipo que implementa realmente la interfaz. Name es el nombre bajo el que debe estar registrado el servicio. Lifetime determina el comportamiento de creación de singleton (devuelve la misma instancia cada vez) o transitoria (crea una instancia nueva cada vez) y así sucesivamente. Elegimos usar una expresión lambda para crear el objeto TypeRegistration, porque hace que sea muy fácil especificar toda esta información en un solo lugar compacto. El siguiente es un ejemplo de la creación de un tipo de registro desde el bloque de acceso a los datos:
yield return new TypeRegistration<Database>(
() => new SqlDatabase(
ConnectionString,
Container.Resolved<IDataInstrumentationProvider>(Name)))
{
Name = Name,
Lifetime = TypeRegistrationLifetime.Transient
};
Este registro de tipo indica que "cuando se solicita una base de datos llamada Name, se devuelve un nuevo objeto SqlDatabase, que está construido con ConnectionString y un IDataInstrumentationProvider”. Lo bueno que tiene usar lambda es que, cuando se escriben los bloques, pudimos crear estas expresiones tal como si estuviésemos creando objetos nuevos directamente. El compilador comprobará el tipo de la expresión, a fin de que no intentemos accidentalmente llamar a un constructor que no existe. Para definir propiedades, sólo use la sintaxis de inicializador de objetos de C# dentro de la expresión lambda. La clase TypeRegistration se preocupa de los detalles de seleccionar en lambda y de extraer la firma del constructor, los parámetros, tipos, etc., para que el autor del configurador no tenga que hacerlo.
Un truco útil que usamos fue llamar a "Container.Resolved". En realidad ese método no hace nada, de hecho, su implementación es simplemente la siguiente:
public static T Resolved<T>(string name)
{
return default(T);
}
¿Por qué está ahí? Recuerde: esta expresión lambda en realidad nunca se ejecuta. En lugar de eso, hacemos un recorrido por la estructura de la expresión en tiempo de ejecución para extraer la información de registro. Este método es simplemente un marcador ampliamente conocido. Cuando encontramos una llamada a Container.Resolved como parámetro, la interpretamos como “resolver este parámetro a través del contenedor”. Hemos encontrado que esta técnica de método de marcador es útil en varios lugares cuando se realiza un trabajo avanzado con árboles de expresión.
Al final, el flujo del archivo de configuración al contenedor configurado se ve como en la figura 2.
Figura 2 Configuración del contenedor
Es importante que expliquemos aquí una decisión de diseño que tomamos. El sistema TypeRegistration no es ni será nunca un propósito general, una abstracción para configurar todo en algún contenedor de DI. Se diseñó específicamente para las necesidades del proyecto Enterprise Library. El equipo de patterns & practices no lo posiciona como una orientación basada en el código. Mientras que la idea básica (extraer la configuración a un modelo abstracto) puede aplicarse de manera general, la implementación específica que aquí se muestra es sólo para Entlib.
Extracción de objetos desde el contenedor
Bueno, ya configuramos nuestro contenedor. Ya tenemos medio camino avanzado. Pero... ¿cómo se hace para que los objetos vuelvan atrás? Las interfaces de los contenedores varían también en esto, aunque afortunadamente no tanto como sus interfaces de configuración.
Por suerte no tuvimos que inventar una nueva abstracción en este punto. Inspirándose en una publicación de blog que Jeremy Miller hizo en el verano de 2008, el grupo de patterns & practices, el equipo de MEF en Microsoft y los autores de varios contenedores diferentes de DI trabajaron en conjunto para definir el denominador común inferior para resolver los objetos fuera de un contenedor. Se publicó en Codeplex y MSDN como el proyecto Common Service Locator. Esta interfaz generó exactamente lo que necesitábamos; desde Enterprise Library, cada vez que necesitábamos sacar un objeto del contenedor, pudimos llamarlo a través de esta interfaz y aislarlo del contenedor específico que se usaba. Por supuesto, la siguiente pregunta es: ¿dónde está el contenedor?
Enterprise Library no tiene ningún tipo de requisito de arranque. Cuando se usan las fachadas estáticas, no necesita llamar a una función de inicialización en ningún lado. La biblioteca original funcionaba extrayendo la configuración cuando la necesitaba por primera vez. Tuvimos que replicar este comportamiento para que la biblioteca estuviera lista para iniciarse cuando se le llamara.
Lo que necesitábamos era un estándar, ampliamente conocido por lograr un contenedor configurado adecuadamente. La biblioteca de Common Service Locator realmente tiene uno de éstos: la propiedad estática ServiceLocator.Current. Decidimos no usarla por un par de razones. La razón principal fue que otras bibliotecas o la misma aplicación podía usar el ServiceLocator.Current. Teníamos que poder configurar el contenedor en el primer acceso de cualquier objeto de Entlib; cualquier otra posibilidad significaba un dolor de cabeza, porque la gente trataba de entender la razón por la que sus contenedores cuidadosamente construidos desaparecían o por qué Entlib funcionaba en la primera llamada, pero dejaba de hacerlo después. La segunda razón tiene que ver con un punto débil en la misma interfaz. No hay manera de consultar la propiedad para saber dónde está configurada. Eso hizo que fuera difícil determinar cuándo configurar el contenedor.
Por lo que construimos nuestra propia propiedad estática: EnterpriseLibraryContainer.Current. También puede definir esta propiedad a partir del código de usuario, pero específicamente es parte de Enterprise Library, por lo que existe una menor posibilidad de generar conflictos con otras bibliotecas o con la aplicación principal. En la primera llamada a una fachada estática, compruebe EnterpriseLibraryContainer.Current. Si está configurada, use lo que está presente. Si no lo está, cree un objeto UnityContainer, configúrelo con un configurador y defínalo como el valor de la propiedad Current.
El resultado es que ahora existen tres maneras diferentes de obtener acceso a la funcionalidad de Enterprise Library. Si usa la API clásica, todo simplemente funciona. De forma subyacente, se creará y usará un contenedor Unity. Si está usando un contenedor de DI diferente en la aplicación y no desea tener Unity en el proceso pero todavía usa la API clásica, puede configurar el contenedor con un configurador, incluirlo en un IServiceLocator, agregarlo en EnterpriseLibraryContainer.Current y luego las fachadas seguirán funcionando. Sólo que ahora usan el contenedor de su elección de manera subyacente. En realidad no suministramos ningún configurador de contenedor en el proyecto principal de Entlib distinto que el que se usa para Unity; esperamos que la comunidad los implemente para otros contenedores.
Una segunda opción es usar directamente EnterpriseLibraryContainer.Current. Puede llamar a GetInstance<T>() para obtener algún objeto Enterprise Library y le dará uno. Y nuevamente puede agregar atrás un contenedor diferente si así lo desea.
Finalmente, puede ocupar directamente el contenedor de su elección. Tendrá que arrancar la configuración de Entlib en el contenedor con un configurador, pero si está usando un contenedor, necesita configurarlo de todas maneras, por lo que no es un requisito nuevo. Desde ese punto, simplemente inserte los objetos de Entlib que desea como dependencias y está listo para funcionar.
¿Cómo lo hicimos?
Volvamos a ver nuestro conjunto de objetivos y veamos cómo se genera este diseño.
-
El código cliente existente no debiera cambiar sólo porque cambiaba la arquitectura. Está bien que sea necesario volver a compilar, pero no está bien solicitar cambios en el código fuente (por supuesto, la API cliente podría cambiar por otras razones). Las API internas o de extensibilidad son parte del juego.
MET. La API original sigue trabajando sin cambios. Si no le interesa la inserción de dependencia, no necesita saber ni ocuparse de cómo se conectan los objetos de manera subyacente.
-
Quitar las estructuras redundantes de la creación de objetos. Sólo deberíamos tener una manera de crear objetos, no dos (o más).
MET. La pila ObjectBuilder se ha eliminado de la base de código; ahora todo se crea a través de los mecanismos de configurador y TypeRegistration. Necesita un configurador por contenedor.
-
Los clientes a los que no les interese DI no deberían verse afectados por Entlib al usarla de manera interna.
MET. DI no se presenta a menos que se requiera.
-
Los clientes a los que sí les interesa DI pueden elegir el contenedor que desean usar y de donde poder extraer sus objetos y los objetos de Entlib.
MET. Puede usar directamente el contenedor de DI de su elección o puede usarlo en segundo plano detrás de las fachadas estáticas.
También terminamos obteniendo algunos beneficios adicionales. La base de código de Entlib se simplificó. Terminamos eliminando unas 200 clases a partir de la implementación original. Después de agregar las partes del registro de tipos, disminuimos unas 80 clases en total después de realizar la refactorización. Además, las clases que agregamos eran más simples que las que eliminamos y la estructura general era considerablemente más coherente, con menos partes movibles o casos especiales.
Otra ventaja fue que la versión refactorizada resultó ser un poco más rápida que la original, con algunas medidas iniciales informales que mostraban una ganancia del 10% en el rendimiento. Una vez que vimos las cifras, esto tuvo real sentido para nosotros. Gran parte de la complejidad del código original se generaba en una serie de optimizaciones del rendimiento que resolvía la lenta implementación de ObjectBuilder. La mayoría de los contenedores de DI tenían realizado un importante trabajo según su rendimiento general. Al regenerar Entlib en la parte superior de un contenedor, podemos aprovechar ese trabajo de rendimiento y nosotros mismos no tenemos que hacer mucho. Como Unity y otros contenedores evolucionan y se optimizan adicionalmente, Entlib debe volverse más rápido sin que eso nos signifique realizar un gran esfuerzo.
Lecciones aprendidas para otras bibliotecas
Enterprise Library es un buen ejemplo de una biblioteca que realmente aprovecha el contenedor de inserción de dependencia sin tener que acoplarse a uno. Si deseara escribir una biblioteca que usa un contenedor de DI que no fuerce su elección en el consumidor, esperamos que nuestro ejemplo pueda inspirarle de alguna manera para crear su diseño. Pienso que nuestros objetivos para realizar el cambio, en especial los dos últimos, son pertinentes para cualquier creador de bibliotecas, no sólo para Entlib:
- Los clientes a los que no les interese DI no deberían verse afectados por Entlib al usarla de manera interna.
- Los clientes a los que sí les interesa DI pueden elegir el contenedor que desean usar y de donde poder extraer sus objetos y los objetos de Entlib.
Cuando diseñe su biblioteca, hay varias preguntas que debe plantearse cuando piense al respecto. Asegúrese de considerar lo siguiente:
- ¿Cómo se arranca su biblioteca? ¿Sus clientes deben realizar algo específico para configurar el código o tiene un punto de entrada estático que simplemente debiera funcionar?
- ¿Cómo realizar el modelo de sus gráficos de objeto para poder configurar un contenedor sin tener que codificar las llamadas a ese contenedor? Inspírese con un vistazo a nuestro sistema TypeRegistration.
- ¿Cómo se administrará el contenedor que está usando? ¿Se controlará internamente o lo administrarán los autores de la llamada? ¿Cómo el autor de la llamada le indica el contenedor que se va a usar?
Encontramos un buen conjunto de respuestas para estas preguntas en nuestro proyecto. Espero que nuestro ejemplo pueda servir de inspiración cuando esté diseñando el suyo.
Chris Tavares es un desarrollador que forma parte del equipo de Microsoft patterns & practices, donde es el director de desarrollo para Enterprise Library y Unity. Antes de llegar a Microsoft, trabajó en consultoría, software retractilado y sistemas incrustados. Publica un blog acerca de Entlib, patterns & practices y temas generales de desarrollo en tavaresstudios.com.