Condividi tramite


Il presente articolo è stato tradotto automaticamente.

CLR

Un approccio senza attributi alla configurazione di MEF

Alok Shriram

 

Managed Extensibility Framework (MEF) è stato progettato per fornire agli sviluppatori Microsoft .net Framework un modo semplice per costruire applicazioni loosely coupled. L'obiettivo primario del MEF in versione 1 era estensibilità, tale che uno sviluppatore di applicazioni potrebbe esporre alcuni punti di estensione per sviluppatori di terze parti, e gli sviluppatori di terze parti potrebbero costruire componenti aggiuntivi o estensioni di tali componenti. Il modello di plug-in Visual Studio per estendere Visual Studio stesso è un caso di utilizzo eccellente di questo, che potete leggere nella pagina MSDN Library "In via di sviluppo Visual Studio Extensions" (bit.ly/IkJQsZ). Questo metodo di estensione di esporre i punti e definizione di plug-in utilizza ciò che è conosciuto come un modello di programmazione attribuito, in cui uno sviluppatore può decorare la proprietà, classi e metodi anche con gli attributi per fare pubblicità sia un requisito per una dipendenza di un tipo specifico o la capacità di soddisfare la dipendenza di un tipo specifico.

Nonostante il fatto che gli attributi sono molto utili per scenari di estendibilità dove è aperto il sistema tipo, they're eccessivo per i sistemi di tipo chiuso sono noti in fase di compilazione. Alcune questioni fondamentali con il modello di programmazione attribuito sono:

  1. Configurazione per molte parti simili comporta un sacco di inutili ripetizioni; Questa è una violazione del principio di Don't Repeat Yourself (secco) e in pratica può condurre ai file di errore e di origine umani sono più difficili da leggere.
  2. Creazione di un'estensione o una parte in .net Framework 4 significa dipendenza dalle assemblee MEF, che lega allo sviluppatore di un quadro di dipendenza specifica iniezione (DI).
  3. Parti che non sono stati progettati con MEF in mente bisogno di avere gli attributi aggiunti a loro al fine di essere adeguatamente identificati nelle applicazioni. Questo può servire come un notevole ostacolo alla loro adozione.

Il .net Framework 4.5 consente di centralizzare la configurazione affinché un set di regole può essere scritte su come componenti e punti di estensione sono create e composte. Questo risultato è ottenuto utilizzando una nuova classe denominata RegistrationBuilder (bit.ly/HsCLrG), che può essere trovato nello spazio dei nomi System.ComponentModel.Composition.Registration. In questo articolo verrà innanzitutto ritengo alcuni motivi per l'utilizzo di un sistema come il MEF. Se sei un veterano MEF, si potrebbe essere in grado di saltare questa parte. Successivamente, don il ruolo di uno sviluppatore che è stato dato un insieme di requisiti e creare una semplice applicazione console utilizzando il modello di programmazione attribuito MEF. Potrai quindi convertire questa applicazione per il modello di convenzione, che mostra come implementare alcuni scenari tipici utilizzo RegistrationBuilder. Infine, verrà illustrato come configurazione guidata da convenzione è già incorporata in modelli di applicazione e come fa usando MEF e DI principi, fuori dalla scatola un compito banale.

Sfondo

Progetti software, cresce in dimensioni e proporzioni, manutenibilità, estensibilità e testabilità diventano principali preoccupazioni. Come progetti software maturo, componenti potrebbe essere necessario essere sostituito o raffinato. Progetti, cresce nell'ambito di requisiti spesso cambiano o aggiunto. La possibilità di aggiungere funzionalità a un progetto di grandi dimensioni in modo semplice è estremamente critica per l'evoluzione di tale prodotto. Inoltre, con il cambiamento sono la norma durante la maggior parte dei cicli di software, la possibilità di testare rapidamente componenti che fanno parte di un prodotto software, indipendentemente da altri componenti, è fondamentale, soprattutto in ambienti dove i componenti dipendenti sono sviluppate in parallelo.

Con queste forze, la nozione di è diventato popolare nei progetti di sviluppo di software su larga scala. L'idea alla base DI è sviluppare componenti che pubblicizzano le dipendenze che hanno bisogno senza in realtà un'istanza di loro, così come le dipendenze soddisfano, e il quadro di dipendenza-iniezione sarà capire e "iniettare" le istanze corrette delle dipendenze nel componente. "Dependency Injection," il numero di settembre 2005 di MSDN Magazine (msdn.microsoft.com/magazine/cc163739), è una risorsa eccellente se avete bisogno di ulteriori informazioni.

Lo Scenario

Ora andiamo per lo scenario descritto in precedenza: Io sono uno sviluppatore guardando una specifica che mi è stata data. Ad alto livello, l'obiettivo della soluzione che ho intenzione di implementare è quello di fornire una previsione meteo a un utente in base al suo codice ZIP. Ecco i passi necessari:

  1. L'applicazione richiede un codice di postale dell'utente.
  2. L'utente immette un codice postale valido.
  3. L'applicazione contatti un provider di servizi Internet meteo per previsioni informazioni.
  4. L'applicazione presenta informazioni previsione formattati per l'utente.

Da un punto di vista di requisiti, è chiaro a questo punto ci sono alcune incognite, o aspetti che hanno il potenziale per cambiare più tardi nel ciclo. Ad esempio, ancora non so quale provider di servizio meteo ho intenzione di utilizzare, o quale metodo io ti impiegano per ottenere i dati dal provider. Così per iniziare a progettare questa applicazione, ' ll break il prodotto in un paio di discreti unità funzionali: WeatherServiceView, IWeatherServiceProvider e interfaccia IDataSource. Il codice per ciascuna di queste classi è mostrato Figura 1, Figura 2 e Figura 3, rispettivamente.

Figura 1 WeatherServiceView — la classe di visualizzazione risultati

[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) dati, l'analisi di servizio

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

Infine, per creare questa gerarchia delle parti, è necessario utilizzare un catalogo dal quale io posso scoprire tutte le parti dell'applicazione, e quindi utilizzare CompositionContainer per ottenere un'istanza di WeatherServiceView, su cui poi posso operare, in questo 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);
  }
}

Tutto il codice che ho presentato finora è abbastanza di base semantica MEF; Se siete poco chiaro su come qualsiasi di questa funziona, date un'occhiata alla pagina MSDN Library "Managed Extensibility Framework Overview" a bit.ly/JLJl8y, che i dettagli del MEF attribuito il modello di programmazione.

Configurazione guidata da convenzione

Ora che ho la versione del mio codice di lavoro, vuole dimostrare come si converte questi pezzi di codice per il modello di convenzione-driven utilizzando RegistrationBuilder. Iniziamo rimuovendo tutte le classi a cui MEF sono stati aggiunti gli attributi. Ad esempio, date un'occhiata al frammento di codice in Figura 4, modificato dai dati WeatherUnderground mostrato nel servizio di analisi Figura 2.

Figura 4 WeatherUnderground dati l'analisi di classe trasformata in semplice classe c#

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

Il codice in Figura 1 e Figura 3 cambierà nello stesso modo come in Figura 4.

Successivamente, utilizzare RegistrationBuilder per definire certe convenzioni al fine di esprimere quello che abbiamo specificato utilizzando gli attributi. Figura 5 Mostra il codice che fa questo.

Figura 5 impostazione convenzioni

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

Ogni dichiarazione di una regola ha due parti distinte. Una parte identifica una classe o un insieme di classi per essere operati su; l'altra specifica gli attributi, i metadati e la condivisione di criteri da applicare per le classi selezionate, proprietà delle classi o costruttori delle classi. Così si può vedere che linee 2, 5 e 8 avviare le tre regole che sto definendo, e la prima parte di ogni regola identifica il tipo su cui il resto della regola sta per essere applicato. Nella linea 5, per esempio, voglio applicare una convenzione per tutti i tipi che derivano da IWeatherServiceProvider.

Ora Let's dare un'occhiata alle regole e mapparle al codice originale attribuito a figure 1, 2 e 3. WeatherFileSource (Figura 3) è stato semplicemente esportato come interfaccia IDataSource. In Figura 5, la regola nelle linee 8 e 9 specifica raccogliendo tutti i tipi che derivano da IDataSource ed esportandoli come contratti di IDataSource. In Figura 2, osservare il codice Esporta tipo IWeatherService­Provider e richiede un'importazione di IDataSource nel relativo costruttore, che è stato decorato con un attributo ImportingConstructor. La regola corrispondente per questo in Figura 5 viene specificato nelle linee 5, 6 e 7. Il pezzo aggiunto qui è il metodo SelectConstructor, che accetta un Func < ConstructorInfo (), ConstructorInfo >. Questo mi dà un modo per specificare un costruttore. È possibile specificare una convenzione, come il costruttore con il numero più piccolo o più grande di argomenti sarà sempre il ImportingConstructor. Nel mio esempio, perché ho un solo costruttore, posso usare il banale caso di prelievo il primo e solo costruttore. Per il codice in Figura 1, la regola in Figura 5 viene specificato nelle linee 2, 3 e 4 ed è simile a quello appena discusso.

Con le regole stabilite, è necessario applicarli ai tipi presenti nell'applicazione. Per fare questo, tutti i cataloghi hanno ora un overload che accetta un RegistrationBuilder come parametro. Così si sarebbe modificare il precedente codice CompositionContainer come mostrato Figura 6.

Figura 6 consumando le convenzioni

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

Insiemi

Ora mi sono fatto e il mio semplice applicazione MEF è installato e funzionante senza attributi. Se solo vita fosse così semplice! Sto ora ha detto il mio app deve essere in grado di supportare più di un servizio meteo e che ha bisogno di visualizzare previsioni da tutti i servizi meteo. Per fortuna, perché ho usato il MEF, non hanno a prendere dal panico. Questo è semplicemente uno scenario con più responsabili dell'implementazione di un'interfaccia, e ho bisogno di scorrerle. Il mio esempio è ormai più di una implementazione di IWeatherServiceProvider e voglio visualizzare i risultati di tutti questi motori meteo. Diamo un'occhiata ai cambiamenti hanno bisogno di fare, come mostrato Figura 7.

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

Questo è tutto! Ho cambiato la classe WeatherServiceView per accettare uno o più implementazioni di IWeatherServiceProvider e nella sezione logica scorrere l'insieme. Le convenzioni che stabilito prima ora cattura tutte le implementazioni di IWeatherServiceProvider ed esportarli.  Tuttavia, qualcosa sembra essere mancante nella mia convenzione: In nessun punto ha avuto aggiungere una convenzione di attributo o equivalente ImportMany quando stavo configurazione della WeatherServiceView. Questo è un po ' di magia RegistrationBuilder in cui figure che, se il parametro è un oggetto IEnumerable <T> su di esso, deve essere un ImportMany, senza dover specificare in modo esplicito. Così utilizzando MEF reso semplice il lavoro di estendere la mia applicazione, e, tramite RegistrationBuilder, fintanto che la nuova versione implementata IWeaterServiceProvider, non ho dovuto fare nulla per farlo funzionare con il mio app. Bella!

Metadata

Un'altra caratteristica davvero utile in MEF è la possibilità di aggiungere metadati alle parti. Supponiamo che per amore della discussione che nell'esempio abbiamo state guardando, il valore restituito dal metodo GetResourcePath (mostrato Figura 2) è posta sotto l'egida del tipo concreto di IDataSource e IWeatherServiceProvider essere utilizzato. Così è possibile definire una convenzione di denominazione specifica che una risorsa sarà nominata come una combinazione di delimitati dal carattere di sottolineatura ("_") il provider del servizio meteo e l'origine dati. Con questa convenzione, il provider di servizi di Weather Underground con una fonte di dati Web avrà il nome di WeatherUnderground_Web_ResourceString. Il codice per questo viene mostrato Figura 8.

Definizione di figura 8 risorse descrizione

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 questa convenzione di denominazione posso ora creare una proprietà nei WeatherUnderground e Google weather service provider che importare tutte queste stringhe di risorsa e, basato sulla loro configurazione attuale, scegliere quella appropriata. Diamo un'occhiata prima a come scrivere la regola RegistrationBuilder per configurare il ResourceInformation come un'esportazione (vedere Figura 9).

Figura 9 la regola per esportare le proprietà e l'aggiunta di metadati

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

Linea 1 identifica semplicemente la classe. Linea 2 definisce un predicato che raccoglie tutte le proprietà in questa classe che contengono ResourceString, che è cosa mia convenzione dettata. L'ultimo argomento di ExportProperties è un'azione < PropertyInfo, ExportBuilder >, in cui specificare che vuoi esportare tutte le proprietà che corrispondono al predicato specificato nella riga 2 come un contratto denominato chiamato ResourceInfo e aggiungere metadati basati sull'analisi del nome della proprietà con i tasti ResourceAffiliation e ResourceLocation. Sul lato che richiede, adesso mi serve aggiungere una proprietà a tutte le implementazioni di IWeatherServiceProvider, come questo:

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

E poi aggiungere la seguente interfaccia per utilizzare fortemente tipizzata dei metadati:

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

Per saperne di più su metadati e metadati fortemente tipizzato, è possibile leggere il tutorial utile a bit.ly/HAOwwW.

Ora aggiungiamo una regola in RegistrationBuilder per importare tutte le parti che hanno il nome di contratto ResourceInfo. Per fare questo, mi prendo la regola esistente da Figura 5 (linee 5-7) e aggiungere la seguente clausola:

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

Linee 8 e 9 ora specificare che tutti i tipi derivati da IWeather­ServiceProvider dovrebbe avere un'importazione applicata su tutte le proprietà di tipo string, e l'importazione dovrebbe essere fatto il nome di contratto ResourceInfo. Quando questa regola viene eseguito, la proprietà precedentemente aggiunta diventa un'importazione per tutti i contratti con il nome ResourceInfo. Posso quindi eseguire query l'enumerazione per filtrare la stringa di risorsa corretta, basata su metadati.

Alla fine di attributi?

Se si considerano i campioni che ho discusso, vedrai che abbiamo davvero non sembrano richiedere più attributi. Qualsiasi cosa che potrebbe fare con il modello di programmazione attribuito può ora essere realizzato utilizzando il modello di convenzione. Citato alcuni casi di uso comune dove può aiutare RegistrationBuilder e grande scrittura di Nicholas Blumhardt su RegistrationBuilder a bit.ly/tVQA1J può dare ulteriori informazioni. Tuttavia, gli attributi possono ancora giocare un ruolo chiave in un mondo MEF guidato dalla convenzione. Un problema significativo con le convenzioni è che sono grandi solo come stai seguirono. Non appena si verifica un'eccezione alla regola, l'overhead di mantenere le convenzioni può ottenere proibitivamente costoso, ma gli attributi possono aiutare con l'override di convenzioni. Supponiamo che una nuova risorsa è stato aggiunto nella classe ResourceInformation, ma il suo nome non ha fatto aderire alla convenzione, come mostrato Figura 10.

Figura 10, si esegue l'override utilizzando gli attributi di convenzioni

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

In Figura 10, si può vedere che la prima parte della convenzione non è corretta, secondo la specifica denominazione. Tuttavia, andando e aggiungendo in modo esplicito un nome del contratto e dei metadati sono corretto, è possibile sovrascrivere o aggiungere le parti scoperte da RegistrationBuilder, rendendo così gli attributi MEF un efficace strumento per specificare le eccezioni alle convenzioni definite da RegistrationBuilder.

Sviluppo senza soluzione di continuità

In questo articolo ho guardato configurazione guidata da convenzione, una nuova funzionalità di MEF esposti nella classe RegistrationBuilder che semplifica notevolmente lo sviluppo associato MEF. Troverete le versioni beta di queste librerie a mef.codeplex.com. Se non avete ancora il .net Framework 4.5, è possibile visitare il sito CodePlex e scaricare i bit.

Ironicamente, RegistrationBuilder può rendere la vostra attività quotidiana di sviluppo ruotano meno intorno MEF e l'uso del MEF nei vostri progetti estremamente senza soluzione di continuità. Un grande esempio di questo è il pacchetto di integrazione costruito in Model-View-Controller (MVC) per il MEF, che può leggere sul Blog del Team BCL in bit.ly/ysWbdL. La versione breve è che è possibile scaricare un pacchetto nella vostra applicazione MVC, e questo imposta il tuo progetto per utilizzare MEF. L'esperienza è che qualunque codice "funziona" e, come si inizia segue la convenzione specificato, è ottenere i benefici di usando il MEF nella vostra applicazione senza dover scrivere una riga di MEF codice voi stessi. Potete trovare ulteriori informazioni sul blog del Team BCL in bit.ly/ukksfe.

Alok Shriram è un program manager per il team di Microsoft .net Framework di Microsoft, dove lavora nel team di librerie di classi Base. Prima che ha lavorato come sviluppatore del team di Office Live, che più tardi divenne il team di Office 365. Dopo un breve periodo di scuola grad presso Università del Nord Carolina, Chapel Hill, è attualmente basato a Seattle. Nel suo tempo libero che ama esplorare tutto ciò che il nord-ovest Pacifico ha da offrire con la sua moglie Mangal. Egli può essere trovato in agguato sul sito CodePlex MEF, su Twitter a twitter.com/alokshriram e occasionalmente postando sul blog .net.

Grazie ai seguenti esperti tecnici per la revisione di questo articolo: Glenn Block, Nicholas Blumhardt e Immo Landwerth