Share via


CLR

Uma abordagem sem atributos para configurar o MEF

Alok Shriram

 

O MEF (Managed Extensibility Framework) foi desenvolvido para dar aos desenvolvedores do Microsoft .NET Framework uma maneira fácil de criar aplicativos flexíveis. O foco principal do MEF na versão 1 foi a extensibilidade, tal que um desenvolvedor de aplicativo pudesse expor certos pontos de extensão a desenvolvedores de terceiros, e os desenvolvedores de terceiros pudessem criar complementos ou extensões para esses componentes. O modelo de plug-in do Visual Studio para estender o próprio Visual Studio é um excelente caso de uso disso, o qual você pode ler a respeito na página da Biblioteca MSDN, “Desenvolvendo extensões do Visual Studio” (bit.ly/IkJQsZ). Esse método de expor pontos de extensão e definir usos de plug-ins é conhecido como modelo de programação atribuída, no qual um desenvolvedor pode decorar propriedades, classes e até mesmo métodos com atributos para anunciar um requisito de uma dependência de um tipo específico ou a capacidade de satisfazer a dependência de um tipo específico.

Apesar dos atributos serem muito úteis em cenários de extensibilidade onde o sistema de tipo é aberto, eles são um exagero para sistemas de tipo fechado que são conhecidos no momento da compilação. Alguns problemas fundamentais com o modelo de programação atribuída são:

  1. A configuração de muitas partes semelhantes envolve muita repetição desnecessária; essa é uma violação do princípio de DRY (Não ser repetitivo) e, na prática, pode levar a erros humanos e arquivos de origem que são mais difíceis de ler.
  2. A criação de uma extensão ou parte no .NET Framework 4 significa dependência de assemblies MEF, que amarra o desenvolvedor a uma estrutura de injeção de dependência (DI) específica.
  3. As partes que não foram desenvolvidas com o MEF em mente precisam ter os atributos adicionados a elas para que sejam identificadas de forma adequada nos aplicativos. Isso pode servir como uma barreira significativa para adoção.

O .NET Framework 4.5 fornece uma maneira de centralizar a configuração de modo que um conjunto de regras possa ser escrito sobre como os pontos de extensão e os componentes são criados e compostos. Isso é obtido com o uso de uma nova classe chamada de RegistrationBuilder (bit.ly/HsCLrG), que pode ser encontrada no namespace System.ComponentModel.Composition.Registration. Neste artigo, considerarei primeiro algumas razões para se usar um sistema como o MEF. Se você for um veterano do MEF, talvez você possa pular esta parte. A seguir, vou assumir o papel de desenvolvedor a quem foi dado um conjunto de requisitos e vou criar um aplicativo de console simples usando o modelo de programação atribuída do MEF. Então, converterei esse aplicativo no modelo baseado em convenção, mostrando como implementar alguns cenários típicos usando RegistrationBuilder. Finalmente, discutirei como a configuração controlada por convenção já está sendo incorporada aos modelos de aplicativos e como ela torna o uso de MEF e dos princípios DI prontos para uso uma tarefa trivial.

Histórico

À medida que os projetos crescem em tamanho e escala, a capacidade de manutenção, de teste e a extensibilidade se tornam preocupações importantes. À medida que os projetos amadurecem, os componentes talvez precisem ser substituídos ou refinados. À medida que os projetos crescem em escopo, os requisitos são alterados ou adicionados com frequência. A capacidade de adicionar funcionalidade a um grande projeto de maneira simples é extremamente crítica para a evolução do produto. Além disso, com alteração sendo a norma durante a maioria dos ciclos de software, a capacidade de rapidamente testar componentes que são uma parte de um produto de software, independentemente de outros componentes, é crucial, especialmente em ambientes em que componentes dependentes são desenvolvidos em paralelo.

Com essas forças motrizes, a noção de DI tornou-se popular em projetos de desenvolvimento de software em grande escala. A ideia por trás de DI é desenvolver componentes que anunciem as dependências de que precisam sem realmente instanciá-las, bem como as dependências que eles satisfazem, e a estrutura de injeção de dependência descobrirá e “injetará” as instâncias corretas das dependências no componente. “Injeção de dependência,” da edição de setembro de 2005 da MSDN Magazine (msdn.microsoft.com/magazine/cc163739), é um excelente recurso caso necessite de mais informações gerais.

O cenário

Vamos agora para o cenário que descrevi anteriormente: Sou um desenvolvedor olhando para uma especificação que me foi dada. Em um alto nível, o objetivo da solução que irei implementar é fornecer uma previsão do tempo a um usuário com base em seu CEP. Aqui estão as etapas necessárias:

  1. O aplicativo solicita um CEP ao usuário.
  2. O usuário insere um CEP válido.
  3. O aplicativo entra em contato com um provedor de serviços meteorológicos da Internet para obter informações sobre previsão.
  4. O aplicativo apresenta informações sobre previsão formatadas para o usuário.

De uma perspectiva de requisitos, está claro que existem algumas incertezas nesse ponto, ou aspectos que têm o potencial de mudar posteriormente no ciclo. Por exemplo, ainda não sei qual provedor de serviços meteorológicos usarei, ou que método empregarei para obter dados do provedor. Assim, para começar a projetar esse aplicativo, quebrarei o produto em algumas unidades funcionais discretas: WeatherServiceView, IWeatherServiceProvider e IDataSource. O código para cada uma dessas classes está mostrado na Figura 1, Figura 2 e Figura 3, respectivamente.

Figura 1 WeatherServiceView—a classe de exibição dos 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
  }
}

Figura 2 IWeatherServiceProvider (WeatherUnderground) Serviço de análise de dados

[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
  }
}

Figura 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();
  }
}

Finalmente, para criar esta hierarquia de partes, preciso usar um Catalog do qual posso descobrir todas as partes do aplicativo e, em seguida, usar o CompositionContainer para obter uma instância de WeatherServiceView, na qual posso então operar, da seguinte forma:

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 o código que apresentei até o momento é semântica de MEF bastante básica; se estiver confuso sobre como qualquer parte disso funciona, dê uma olhada na página da Biblioteca MSDN “Visão geral de MEF (Managed Extensibility Framework)” em bit.ly/JLJl8y, que detalha o modelo de programação atribuída de MEF.

Configuração controlada por convenção

Agora que tenho a versão atribuída do meu código funcionando, quero demonstrar como converter essas partes de código em modelo controlado por convenção usando RegistrationBuilder. Vamos começar removendo todas as classes às quais os atributos de MEF foram adicionados. Como exemplo, dê uma olhada no fragmento de código na Figura 4, modificado do serviço de análise de dados WeatherUnderground mostrado na Figura 2.

Figura 4 WeatherUnderground Classe de análise de dados convertida em classe de C# simples

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;
  }
      ...
}

O código na Figura 1 e na Figura 3 será alterado do mesmo modo que na Figura 4.

Em seguida, uso RegistrationBuilder para definir certas convenções para expressar o que especificamos usando os atributos. A Figura 5 mostra o código que faz isso.

Figura 5 Configurando as convenções

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 declaração de uma regra tem duas partes distintas. Uma parte identifica a classe ou um conjunto de classes em que operar; a outra especifica os atributos, metadados e as políticas de compartilhamento a serem aplicados às classes selecionadas, às propriedades das classes ou aos construtores das classes. Assim, você pode ver que as linhas 2, 5 e 8 começam as três regras que estou definindo, e a primeira parte de cada regra identifica o tipo no qual o resto da regra será aplicada. Na linha 5, por exemplo, quero aplicar uma convenção para todos os tipos que são derivados de IWeatherServiceProvider.

Agora, vamos dar uma olhada nas regras e mapeá-las de volta ao código atribuído original nas Figuras 1, 2 e 3. WeatherFileSource (Figura 3) foi apenas exportado como IDataSource. Na Figura 5, a regra nas linhas 8 e 9 especifica selecionar todos os tipos derivados de IDataSource e exportá-los como contratos de IDataSource. Na Figura 2, observe que o código exporta o tipo IWeatherService­Provider e requer uma importação de IDataSource em seu construtor, que foi decorado com um atributo ImportingConstructor. A regra correspondente para isso na Figura 5 é especificada nas linhas 5, 6 e 7. A parte adicionada aqui é o método SelectConstructor, que aceita um Func<ConstructorInfo[], ConstructorInfo>. Isso me dá uma maneira de especificar um construtor. É possível especificar uma convenção, por exemplo, o construtor com o menor ou maior número de argumentos sempre será o ImportingConstructor. No meu exemplo, como tenho apenas um construtor, posso usar o caso trivial de selecionar o primeiro e único construtor. Para o código na Figura 1, a regra na Figura 5 está especificada nas linhas 2, 3 e 4, e é similar à que acabamos de discutir.

Com as regras estabelecidas, preciso aplicá-las aos tipos presentes no aplicativo. Para fazer isso, todos os catálogos agora têm uma sobrecarga que aceita um RegistrationBuilder como parâmetro. Assim, você modificaria o código CompositionContainer anterior como mostrado na Figura 6.

Figure 6 Consumindo as convenções

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);
  }
}

Coleções

Bem, terminei e meu aplicativo MEF simples está em funcionamento sem atributos. Se a vida fosse tão simples! Agora fui informado de que meu aplicativo precisa ser capaz de dar suporte a mais de um serviço meteorológico e que ele precisa exibir previsões de todos esses serviços. Felizmente, pois usando MEF não preciso me apavorar. Esse é apenas um cenário com diversos implementadores de uma interface e preciso iterar neles. Meu exemplo agora tem mais de uma implementação de IWeatherServiceProvider e quero exibir os resultados de todos esses mecanismos meteorológicos. Vamos dar uma olhada nas alterações que tenho de fazer, como mostrado na Figura 7.

Figura 7 Habilitando diversos 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));
    }
    }
}

E pronto! Alterei a classe WeatherServiceView para aceitar uma ou mais implementações de IWeatherServiceProvider e na seção de lógica iterei nessa coleção. As convenções que estabeleci anteriormente irão capturar agora todas as implementações de IWeatherServiceProvider e exportá-las. No entanto, parece que algo está faltando em minha convenção: Em nenhum momento tive de adicionar um atributo ImportMany ou convenção equivalente ao configurar o WeatherServiceView. Isso é um pouco de mágica de RegistrationBuilder na qual ele calcula que se seu parâmetro tem um IEnumerable<T> nele, ele deve ser um ImportMany, sem ter de especificá-lo explicitamente. Portanto, usar MEF simplificou o trabalho de extensão de meu aplicativo e, usando RegistrationBuilder, considerando que a nova versão implementou IWeaterServiceProvider, não tive de fazer nada para fazê-lo funcionar com meu aplicativo. Lindo!

Metadados

Outro recurso realmente útil no MEF é a capacidade de adicionar metadados a partes. Vamos supor, para fins explicativos, que no exemplo que estamos vendo, o valor retornado pelo método GetResourcePath (mostrado na Figura 2) é governado pelo tipos concretos de IDataSource e IWeatherServiceProvider sendo usados. Assim, defino uma convenção de nomenclatura especificando que um recurso será nomeado como uma combinação delimitada por sublinhado (“_”) do provedor de serviço meteorológico e da fonte de dados. Com essa convenção, o provedor Weather Underground com uma fonte de dados da Web terá o nome WeatherUnderground_Web_ResourceString. O código para isso é mostrado na Figura 8.

Figura 8 Definição da descrição do 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/"; }
  }
}

Usando essa convenção de nomenclatura, posso agora criar uma propriedade nos provedores de serviços meteorológicos WeatherUnderground e Google que importará todas essas cadeias de caracteres de recurso e, com base em suas configurações atuais, selecionar a adequada. Vamos ver primeiro como escrever a regra RegistrationBuilder para configurar ResourceInformation como Export (consulte a Figura 9).

Figura 9 A regra para exportar propriedades e adicionar metadados

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]);
     });

A linha 1 simplesmente identifica a classe. A linha 2 define um predicado que seleciona todas as propriedades dessa classe que contêm ResourceString, que é o que minha convenção determinou. O último argumento de ExportProperties é Action<PropertyInfo,ExportBuilder>, no qual especifico que quero exportar todas as propriedades que correspondem ao predicado especificado na linha 2 como um contrato nomeado chamado de ResourceInfo, e quero adicionar metadados com base na análise do nome da propriedade usando as chaves ResourceAffiliation e ResourceLocation. No lado consumidor, preciso agora adicionar uma propriedade a todas as implementações de IWeatherServiceProvider, da seguinte forma:

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

E então adicionar a seguinte interface para usar metadados fortemente tipados:

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

Para saber mais sobre metadados e metadados fortemente tipados, leia o útil tutorial em bit.ly/HAOwwW.

Vamos agora adicionar uma regra a RegistrationBuilder para importar todas as partes que têm o nome de contrato ResourceInfo. Para fazer isso, pegarei a regra existente da Figura 5 (linhas 5-7) e adicionarei a seguinte cláusula:

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

As linhas 8 e 9 agora especificam que todos os tipos derivados de IWeather­ServiceProvider devem ter um Import aplicado a todas as propriedades da cadeia de caracteres de tipo, e a importação deve ser feita no nome de contrato ResourceInfo. Quando essa regra é executada, a propriedade anteriormente adicionada torna-se um Import para todos os contratos com o nome ResourceInfo. Posso então consultar a enumeração para filtrar a cadeia de caracteres de recurso correta, com base nos metadados.

O fim dos atributos?

Se você considerar os exemplos que discuti, verá que na realidade parece que não precisamos mais de atributos. Tudo que você podia fazer com o modelo de programação atribuída agora pode ser obtido usando o modelo baseado em convenção. Mencionei alguns casos de uso comuns em que RegistrationBuilder pode ajudar, e a excelente reportagem de Nicholas Blumhardt sobre RegistrationBuilder em bit.ly/tVQA1J pode fornecer mais informações. No entanto, os atributos ainda têm um papel importante em um mundo MEF controlado por convenção. Um problema significativo com convenções é que elas são ótimas apenas se seguidas. Assim que uma exceção à regra acontece, a sobrecarga de manter as convenções pode ficar proibitivamente cara, mas os atributos podem ajudar na substituição das convenções. Vamos supor que um novo recurso foi adicionado à classe ResourceInformation, mas seu nome não seguiu a convenção, conforme mostrado na Figura 10.  

Figura 10 Substituindo convenções com o uso de 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"; }
  }
}

Na Figura 10, pode ser visto que a primeira parte da convenção está incorreta, de acordo com a especificação de nomenclatura. No entanto, entrando e adicionando explicitamente um nome de contrato e metadados que estão corretos, você pode substituir ou adicionar às partes descobertas por RegistrationBuilder, tornado então os atributos de MEF uma ferramenta efetiva para especificar exceções às convenções definidas por RegistrationBuilder.

Desenvolvimento contínuo

Neste artigo vimos a configuração controlada por convenção, um novo recurso do MEF exposto na classe RegistrationBuilder que simplifica bastante o desenvolvimento associado a MEF. Versões beta dessas bibliotecas podem ser encontradas em mef.codeplex.com. Se você ainda não tiver o .NET Framework 4.5, poderá ir para o site CodePlex e baixá-lo. 

Ironicamente, o RegistrationBuilder pode fazer suas atividades de desenvolvimento do dia a dia girar menos em torno do MEF, e seu uso do MEF em seus projetos extremamente contínuo. Um grande exemplo disso é o pacote de integração desenvolvido em Model-View-Controller (MVC) para MEF, sobre o qual você pode ler no blog da equipe da BCL em bit.ly/ysWbdL. O resumo é que você pode baixar um pacote em seu aplicativo MVC e isso configura seu projeto para usar MEF. A experiência é que qualquer código que você tenha “simplesmente funciona” e, conforme começa a seguir a convenção especificada, você obtém os benefícios do uso de MEF em seu aplicativo sem ter de escrever uma linha de código de MEF. Você pode descobrir mais sobre isso no blog da equipe da BCL em bit.ly/ukksfe.

Alok Shriram é gerente de programas da equipe do Microsoft .NET Framework na Microsoft e trabalha na equipe das Bibliotecas de classes base. Antes disso, trabalhou como desenvolvedor na equipe do Office Live, que posteriormente tornou-se a equipe do Office 365. Depois de uma pós-graduação na Universidade da Carolina do Norte, Chappel Hill, atualmente ele está baseado em seattle. Em seu tempo livre gosta de explorar tudo o que o noroeste do Pacífico tem para oferecer com sua esposa Mangal. Ele pode ser encontrado escondido no site CodePlex do MEF, no Twitter em twitter.com/alokshriram e, ocasionalmente, postando no blog do .NET.

Agradecemos aos seguintes especialistas técnicos pela revisão deste artigo: Glenn Block, Nicholas Blumhardt e Immo Landwerth