Compartir a través de


CLR

Una forma de configurar MEF sin atributos

Alok Shriram

 

Managed Extensibility Framework (MEF) se diseñó para brindar a los desarrolladores de Microsoft .NET Framework una forma fácil de crear aplicaciones con un acoplamiento flexible. La primera versión de MEF se centró principalmente en la extensibilidad, para permitir que los desarrolladores de aplicaciones expongan ciertos puntos de extensión para que otros desarrolladores puedan crear complementos o extensiones para estos componentes. El modelo de complementos de Visual Studio para extender el Visual Studio en sí es un excelente ejemplo de esto; puede informarse mejor en la página de MSDN Library “Desarrollo de extensiones para Visual Studio” (bit.ly/IkJQsZ). El método para exponer los puntos de extensión y definir complementos emplea lo que conocemos como un modelo de programación atributivo, donde el desarrollador puede decorar las propiedades, clases e incluso métodos con atributos para anunciar el requerimiento de una dependencia de cierto tipo o la capacidad de satisfacer una dependencia de cierto tipo.

Aunque los atributos son muy útiles para las situaciones de extensibilidad donde el sistema de tipos es abierto, resultan excesivos en los sistemas de tipos cerrados que ya se conocen en el momento de la compilación. Algunos de los problemas fundamentales del modelo de programación atributivo son:

  1. La configuración de varias partes similares implica una gran cantidad de repeticiones innecesarias; esto es una infracción a la regla del No te repitas (DRY) y en la práctica puede conducir a errores humanos y a un código fuente que resulta difícil de entender.
  2. La creación de una extensión o parte en .NET Framework 4 implica una dependencia de los ensamblados MEF, lo que amarra al desarrollador a un marco de inserción de dependencias (DI) determinado.
  3. En el caso de las piezas que no se diseñaron pensando en MEF, hay que agregar los atributos para poder identificarlos debidamente en las aplicaciones. Esto puede ser un obstáculo significativo para la adopción.

.NET Framework 4.5 ofrece una forma para centralizar la configuración, lo que permite escribir un conjunto de reglas sobre cómo se crean y componen los puntos y componentes de extensión. Esto se logra mediante una clase nueva llamada RegistrationBuilder (bit.ly/HsCLrG) que se encuentra en el espacio de nombres System.ComponentModel.Composition.Registration. En este artículo evaluaré algunas razones para usar un sistema como MEF. Los veteranos de MEF pueden pasar por alto esta parte. Luego me pondré en el papel de un desarrollador que recibió un conjunto de requisitos y crearé una aplicación de consola sencilla con el modelo de programación atributivo de MEF. Luego convertiré la aplicación al modelo basado en convenciones y mostraré cómo implementar algunos casos típicos con RegistrationBuilder. Por último, examinaré cómo la configuración basada en convenciones ya se está incorporando en los modelos de aplicación y cómo esto ha provocado que el uso de los principios de MEF y DI desde el primer momento sea completamente trivial.

Información previa

A medida que los proyectos de software crecen en tamaño y escala, la facilidad de mantenimiento, extensibilidad y pruebas se vuelven aspectos fundamentales. A medida que los proyectos de software maduran, es probable que haya que reemplazar o depurar los componentes. A medida que se amplía el alcance de los proyectos, cambian los requisitos o se agregan nuevos. La posibilidad de agregar funciones a un proyecto grande de manera sencilla es extremadamente importante para la evolución del producto. Por lo demás, con unos ciclos de desarrollo donde los cambios son la norma, la posibilidad de probar de manera rápida los componentes que forman parte de un producto de software, independientemente de los otros componentes es determinante; sobre todo en los entornos donde los componentes dependientes se desarrollan en paralelo.

Debido a estas fuerzas impulsoras, la idea de la DI se ha popularizado en los proyectos de desarrollo de software de gran escala. La idea detrás de la DI es desarrollar componentes que anuncien las dependencias requeridas, sin instanciarlas, además de las dependencias que satisfacen, y el marco de inserción de dependencias luego se las arreglará para “insertar” las instancias correctas de las dependencias en el componente. “Inserción de dependencias” de la edición de septiembre de 2005 de MSDN Magazine (msdn.microsoft.com/magazine/cc163739) es un recurso excelente si necesita más información de fondo.

El contexto

Veamos ahora la situación que describí previamente: soy un desarrollador que está mirando una especificación que recibí. En el nivel superior, la meta de la solución que implementaré es entregar un pronóstico del tiempo al usuario en función de su código postal. Estos son los pasos necesarios:

  1. La aplicación solicita el código postal del usuario.
  2. El usuario escribe un código postal válido.
  3. La aplicación se pone en contacto a un proveedor de servicio meteorológico por Internet para obtener el pronóstico.
  4. La aplicación presenta la información del pronóstico formateada al usuario.

Desde el punto de vista de los requisitos, queda claro que a estas alturas existen algunas incógnitas o aspectos que podrían cambiar más adelante. Por ejemplo, no conozco aún el proveedor de servicio meteorológico que emplearé ni el método que usaré para obtener los datos del proveedor. Por lo tanto, para comenzar a diseñar la aplicación dividiré el producto en varias unidades funcionales discretas: WeatherServiceView, IWeatherServiceProvider y IDataSource. El código de cada una de estas clases aparece en la Ilustración 1, Ilustración 2 e Ilustración 3, respectivamente.

Ilustración 1 WeatherServiceView: la clase que presenta los resultados

[Export]
public class WeatherServiceView
{
  private IWeatherServiceProvider _provider;
  [ImportingConstructor]
  public WeatherServiceView(IWeatherServiceProvider providers)
  {
    _providers = providers;
  }
  public void GetWeatherForecast(int zipCode)
  {
    var result=_provider.GetWeatherForecast(zipCode);
      // Some display logic
  }
}

Ilustración 2 IWeatherServiceProvider: servicio de análisis de datos (WeatherUnderground)

[Export(typeof(IWeatherServiceProvider))]
class WeatherUndergroundServiceProvider:IWeatherServiceProvider
{  private IDataSource _source;
  [ImportingConstructor]
  public WeatherUndergroundServiceProvider(IDataSource source)
  {
    _source = source;
  }
  public string GetWeatherForecast(int zipCode)
  {
    string val = _source.GetData(GetResourcePath(zipCode));
      // Some parsing logic here
    return result;
  }
  private string GetResourcePath(int zipCode)
  {
    // Some logic to get the resource location
  }
}

Ilustración 3 IDataSource (WeatherFileSource)

[Export(typeof(IDataSource))]
class WeatherFileSource :IDataSource
{
  public string GetData(string resourceLocation)
  {
    Console.WriteLine("Opened ----> File Weather Source ");
    StringBuilder builder = new StringBuilder();
    using (var reader = new StreamReader(resourceLocation))
    {
      string line;
      while((line=reader.ReadLine())!=null)
      {
        builder.Append(line);
      }
    }
    return builder.ToString();
  }
}

Por último, para crear esta jerarquía de piezas, debo usar un Catalog que me permita descubrir todas las piezas de la aplicación y luego usar CompositionContainer para obtener una instancia de WeatherServiceView, en el cual puedo operar, del siguiente modo:

class Program
{
  static void Main(string[] args)
  {
    AssemblyCatalog cat = 
      new AssemblyCatalog(typeof(Program).Assembly);
    CompositionContainer container = 
      new CompositionContainer(cat);           
    WeatherServiceView forecaster =
      container.GetExportedValue<WeatherServiceView>();
    // Accept a ZIP code and call the viewer
    forecaster.GetWeatherForecast(zipCode);
  }
}

Todo el código que presenté hasta el momento tiene una semántica MEF bastante básica; si no tiene claro cómo funciona, eche una mirada a la página de MSDN Library “Información general sobre Managed Extensibility Framework” en bit.ly/JLJl8y, que explica a fondo el modelo de programación atributivo de MEF.

Configuración controlada por convenciones

Ahora que tengo la versión atributiva del código, quiero ilustrar cómo convertir estas piezas de código al modelo controlado por convenciones mediante RegistrationBuilder. Comencemos por eliminar todas las clases a las que se agregaron atributos MEF. Por ejemplo, eche una mirada al fragmento que aparece en la Ilustración 4, modificado del servicio de análisis de datos WeatherUnderground que aparece en la Ilustración 2.

Ilustración 4 Clase de análisis de datos WeatherUnderground convertida a una clase C# sencilla

class WeatherUndergroundServiceProvider:IWeatherServiceProvider
{
  private IDataSource _source;
  public WeatherUndergroundServiceProvider(IDataSource source)
  {
    _source = source;
  }
  public string GetWeatherForecast(int zipCode)
  {
    string val = _source.GetData(GetResourcePath(zipCode));
    // Some parsing logic here
    return result;
  }
      ...
}

El código de la Ilustración 1 y la Ilustración 3 cambiará igual que el de la Ilustración 4.

Luego, uso RegistrationBuilder para definir ciertas convenciones para expresar lo que habíamos especificado con los atributos. En la Ilustración 5 aparece el código correspondiente.

Ilustración 5 Configuración de las convenciones

RegistrationBuilder builder = new RegistrationBuilder();
    builder.ForType<WeatherServiceView>()
      .Export()
      .SelectConstructor(cinfos => cinfos[0]);
    builder.ForTypesDerivedFrom<IWeatherServiceProvider>()
      .Export<IWeatherServiceProvider>()
      .SelectConstructor(cinfo => cinfo[0]);
    builder.ForTypesDerivedFrom<IDataSource>()
      .Export<IDataSource>();

Cada declaración de una regla se compone de dos partes definidas. Una parte identifica una clase o conjunto de clases en las que se opera; la otra parte especifica los atributos, metadatos y directivas para compartir que se aplican a las clases seleccionadas, propiedades de las clases o constructores de las clases. Por lo tanto, observamos que las líneas 2, 5 y 8 dan inicio a las tres reglas que defino, y la primera parte de cada regla identifica el tipo en el que se aplicará el resto de la regla. En la línea 5, por ejemplo, quiero aplicar una convención para todos los tipos que se derivan de IWeatherServiceProvider.

Examinemos ahora las reglas y asignémoslas al código atributivo original de las Ilustraciones 1, 2 y 3. WeatherFileSource (Ilustración 3) simplemente se exportó en forma de IDataSource. En la Ilustración 5, la regla de las líneas 8 y 9 especifica que se deben seleccionar todos los tipos que se derivan de IDataSource y se deben exportar como contratos de IDataSource. En la Ilustración 2, observe que el código exporta el tipo IWeatherService­Provider y requiere que se importe IDataSource en el constructor, que se había decorado con un atributo ImportingConstructor. La regla correspondiente en la Ilustración 5 se especifica en las líneas 5, 6 y 7. La pieza que se agregó aquí es el método SelectConstructor, que acepta Func<ConstructorInfo[], ConstructorInfo>. Así puedo especificar un constructor. Podemos especificar una convención: por ejemplo que el constructor con la menor o mayor cantidad de argumentos siempre será ImportingConstructor. En el ejemplo, como solo tengo un constructor, puedo usar el caso trivial de seleccionar el primer (y único) constructor. Para el código de la Ilustración 1, la regla en la Ilustración 5 se especifica en las líneas 2, 3 y 4, y es parecida a la que acabamos de analizar.

Ahora que establecimos todas las reglas, debo aplicarlas a los tipos presentes en la aplicación. Para esto, todos los catálogos ahora tienen una sobrecarga que acepta un RegistrationBuilder como parámetro. Por lo tanto, debemos modificar el código de CompositionContainer tal como podemos apreciar en la Ilustración 6.

Ilustración 6 Consumo de las convenciones

class Program
{
  static void Main(string[] args)
  {
    // Put the code to build the RegistrationBuilder here
    AssemblyCatalog cat = 
      new AssemblyCatalog(typeof(Program).Assembly,builder);
    CompositionContainer container = new CompositionContainer(cat);           
    WeatherServiceView forecaster =
      container.GetExportedValue<WeatherServiceView>();
    // Accept a ZIP code and call the viewer
    forecaster.GetWeatherForecast(zipCode);
  }
}

Colecciones

Ya estoy listo, y mi aplicación MEF sencilla funciona sin atributos. ¡Si solo fuera así de fácil! Ahora me dicen que la aplicación debe poder operar con más de un servicio meteorológico y que debe presentar los pronósticos de todos ellos. Afortunadamente, como usé MEF, no tengo de qué preocuparme. Esto no es más que una situación con varios implementadores de una interfaz, donde debo recorrerlos todos. Mi ejemplo ahora tiene más de una implementación de IWeatherServiceProvider y deseo mostrar los resultados de todas estas máquinas meteorológicas. Echemos una mirada a los cambios que debo realizar, como se aprecia en la Ilustración 7.

Ilustración 7 Habilitación de varios IWeatherServiceProviders

public class WeatherServiceView
{
  private IEnumerable<IWeatherServiceProvider> _providers;
  public WeatherServiceView(IEnumerable<IWeatherServiceProvider> providers)
  {
    _providers = providers;
  }
  public void GetWeatherForecast(int zipCode)
  {
    foreach (var _provider in _providers)
    {
      Console.WriteLine("Weather Forecast");
      Console.WriteLine(_provider.GetWeatherForecast(zipCode));
    }
    }
}

Eso es todo. Cambié la clase WeatherServiceView para que acepte una o más implementaciones de IWeatherServiceProvider y en la sección de la lógica iteré por la colección. Las convenciones que establecí previamente, ahora capturan todas las implementaciones de IWeatherServiceProvider y las exportan. Sin embargo, algo parece que falta en mi convención: en ningún momento tuve que agregar un atributo ImportMany o una convención equivalente cuando configuré WeatherServiceView. Aquí RegistrationBuilder realiza un poco de magia, ya que se percata de que si el parámetro tiene un IEnumerable<T>, entonces debe ser un ImportMany, sin que nosotros se lo digamos en forma explícita. Por lo tanto, al usar MEF pude extender la aplicación fácilmente, y al usar RegistrationBuilder (siempre que la versión nueva implementara IWeaterServiceProvider) no tuve que hacer nada para que funcione con mi aplicación. ¡Hermoso!

Metadatos

Otra característica realmente útil de MEF es la posibilidad de agregar metadatos a las piezas. Supongamos para este análisis que en el ejemplo que hemos estado viendo el valor devuelto por el método GetResourcePath (que aparece en la Ilustración 2) está gobernado por el tipo concreto de los IDataSource y IWeatherServiceProvider que se usan. Por lo tanto, defino una convención de nomenclatura que especifica que los nombres de los recursos emplearán una combinación del proveedor del servicio meteorológico y del origen de datos, separados por guiones bajos (“_”). Con esta convención, el proveedor de servicios Weather Underground con un origen de datos Web recibe el nombre WeatherUnderground_Web_ResourceString. El código correspondiente aparece en la Ilustración 8.

Ilustración 8 Definición de la descripción del recurso

public class ResourceInformation
{
  public string Google_Web_ResourceString
  {
    get { return "http://www.google.com/ig/api?weather="; }
  }
  public string Google_File_ResourceString
  {
    get { return @".\GoogleWeather.txt"; }
  }
  public string WeatherUnderground_Web_ResourceString
  {
    get { return
      "http://api.wunderground.com/api/96863c0d67baa805/conditions/q/"; }
  }
}

Con esta convención de nomenclatura, ahora puedo crear una propiedad en los proveedores de servicio WeatherUnderground y Google que importará todas estas cadenas de recurso y elegir el adecuado a partir de las configuraciones actuales. Veamos primero cómo escribir la regla de RegistrationBuilder para configurar ResourceInformation como un Export (consulte la Ilustración 9).

Ilustración 9 Regla para exportar propiedades y agregar metadatos

builder.ForType<ResourceInformation>()
       .ExportProperties(pinfo => 
       pinfo.Name.Contains("ResourceString"),
    (pinfo, eb) =>
      {
        eb.AsContractName("ResourceInfo");
        string[] arr = pinfo.Name.Split(new char[] { '_' },
          StringSplitOptions.RemoveEmptyEntries);
        eb.AddMetadata("ResourceAffiliation", arr[0]);
        eb.AddMetadata("ResourceLocation", arr[1]);
     });

La línea 1 simplemente identifica la clase. La línea 2 define un predicado que elige todas las propiedades de la clase que contienen ResourceString, que es lo que dictó mi convención. El último argumento de ExportProperties es un Action<PropertyInfo,ExportBuilder>, donde especifico que quiero exportar todas las propiedades que coinciden con el predicado especificado en la línea 2, llamado ResourceInfo, y quiero agregar metadatos en función del análisis del nombre de la propiedad con las claves ResourceAffiliation y ResourceLocation. En el lado del consumidor, ahora debo agregar una propiedad a todas las implementaciones de IWeatherServiceProvider, del siguiente modo:

public IEnumerable<Lazy<string, IServiceDescription>>
 WeatherDataSources { get; set; }

Y agregar luego la siguiente interfaz para emplear metadatos tipificados en forma inflexible:

public interface IServiceDescription
  {
    string ResourceAffiliation { get; }
    string ResourceLocation { get; }   
  }

Para aprender más sobre los metadatos y metadatos tipificados en forma inflexible, puede leer este tutorial útil en bit.ly/HAOwwW.

Agreguemos ahora una regla en RegistrationBuilder para importar todas las piezas que tienen el contrato llamado ResourceInfo. Para esto, tomo la regla existente de la Ilustración 5 (líneas 5 a 7) y agrego la siguiente cláusula:

builder.ForTypesDerivedFrom<IWeatherServiceProvider>()
       .Export<IWeatherServiceProvider>()
       .SelectConstructor(cinfo => cinfo[0]);
       .ImportProperties<string>(pinfo => true,
                                (pinfo, ib) =>
                                 ib.AsContractName("ResourceInfo"))

Las líneas 8 y 9 ahora especifican que se debe aplicar un Import en todos los tipos derivados de IWeather­ServiceProvider en todas las propiedades del tipo string y que la importación se debe realizar en el contrato llamado ResourceInfo. Cuando se ejecuta esta regla, la propiedad que se agregó previamente se convierte en un Import para todos los contratos llamados ResourceInfo. Luego puedo consultar la enumeración para filtrar la cadena de recurso correcta, a partir de los metadatos.

¿La hora final de los atributos?

Si tomamos en cuenta los ejemplos que analizamos, nos quedamos con la impresión que en verdad ya no necesitamos los atributos. Todo lo que se podía hacer con el modelo de programación atributivo ahora se puede lograr con el modelo basado en convenciones. Mencioné algunos casos de uso comunes donde puede servir RegistrationBuilder, y el excelente artículo de Nicholas Blumhardt’s sobre RegistrationBuilder en bit.ly/tVQA1J puede entregarle más información. Sin embargo, los atributos siguen jugando un papel clave en el mundo de MEF controlado por convenciones. Un problema significativo con las convenciones es que solo funcionan mientras se respetan. En cuanto se genera una excepción a la regla, la sobrecarga de tener que mantener las convenciones se puede volver prohibitiva; pero los atributos permiten reemplazar las convenciones. Supongamos que se agrega un recurso nuevo a la clase ResourceInformation, pero el nombre no se rige según la convención, como podemos ver en la Ilustración 10.  

Ilustración 10 Reemplazo de convenciones mediante atributos

public class ResourceInformation
{
  public string Google_Web_ResourceString
  {
    get { return "http://www.google.com/ig/api?weather="; }
  }
  public string Google_File_ResourceString
  {
    get  { return @".\GoogleWeather.txt"; }
  }
  public string WeatherUnderground_Web_ResourceString
  {
    get { return "http://api.wunderground.com/api/96863c0d67baa805/conditions/q/"; }
  }
  [Export("ResourceInfo")]
  [ExportMetadata("ResourceAffiliation", "WeatherUnderground")]
  [ExportMetadata("ResourceLocation", "File")]
  public string WunderGround_File_ResourceString
  {
    get { return @".\Wunder.txt"; }
  }
}

En la Ilustración 10 vemos que la primera parte de la convención no es correcta, según la especificación de nomenclatura. Sin embargo, al entrar específicamente y agregar un nombre de contrato y metadatos correctos, podemos reemplazar o ampliar las partes descubiertas por RegistrationBuilder, de modo que los atributos de MEF se convierten en una herramienta eficaz para especificar excepciones a las convenciones definidas por RegistrationBuilder.

Desarrollo fluido

En este artículo analicé la configuración controlada por convenciones, una característica nueva de MEF expuesta en la clase RegistrationBuilder, que agiliza enormemente el desarrollo asociado con MEF. Encontrará versiones beta de estas bibliotecas en mef.codeplex.com. Si no tiene el .NET Framework 4.5, puede visitar el sitio CodePlex y descargar las piezas. 

Paradójicamente, RegistrationBuilder permite que sus actividades de desarrollo en el día a día giren menos en torno a MEF, y permiten que el uso de MEF en sus proyectos sea extremadamente fluido. Un buen ejemplo de esto es el excelente paquete de integración Model-View-Controller (MVC) integrado para MEF, sobre el cual puede leer más en el blog del equipo de BCL en bit.ly/ysWbdL. En pocas palabras, puede descargar un paquete en la aplicación MVC, y este configura el proyecto para que use MEF. La experiencia es que cualquier código existente “simplemente funciona” y, a medida que comienza a seguir las convenciones especificadas, recibe los beneficios de MEF en la aplicación, sin tener que escribir ni una sola línea de MEF. Puede encontrar más información en el blog del equipo de BCL en bit.ly/ukksfe.

Alok Shriram es jefe de programas del equipo de Microsoft .NET Framework en Microsoft, donde trabaja en el equipo de las Bibliotecas de clases base. Previamente trabajó como desarrollador en el equipo de Office Live, que luego se convirtió en el equipo de Office 365. Después de un período en la escuela de pregrado de la Universidad de Carolina del Norte, Chapel Hill, actualmente vive en Seattle. En los tiempos libres le gusta explorar el Noreste Pacífico con su esposa Mangal. Lo puede encontrar en el sitio CodePlex de MEF, en Twitter en twitter.com/alokshriram y ocasionalmente escribe en el blog de .NET.

Gracias a los siguientes expertos técnicos por su ayuda en la revisión de este artículo: Glenn Block, Nicholas Blumhardt e Immo Landwerth