Mehr als die Summe seiner Teile

Neuerungen im Managed Extensibility Framework (MEF) 2
Kommentare

MEF 2 bringt ein API zum Definieren von Parts, die eine Alternative zur Konfiguration über Attribute darstellen. Daneben existiert nun eine leichtgewichtige Variante von MEF, die sich für den Einsatz in Webanwendungen anbietet und auch von Windows-8-Apps genutzt werden kann. Abgesehen davon kann nun auch die Instanziierung von Imports feingranularer gesteuert werden.

Architektur von MEF

Die Architektur von PEX sieht die Möglichkeit vor, so genannte Parts zu definieren. Dabei handelt es sich um Programmteile, die entweder anderen Parts bestimmte Funktionalitäten bereitstellen oder bestimmte Funktionalitäten von anderen Parts benötigen. Im ersten Fall ist von Exports die Rede; im zweiten Fall könnte man von importierenden Komponenten sprechen. Wie der Name vermuten lässt, importieren diese Komponenten Exports, wobei in dieser Rolle Exports als „Imports“ bezeichnet werden. Dabei ist es nicht notwendig, dass die einzelnen Parts einander direkt kennen, denn das Verdrahten der einzelnen Parts wird von einem Composition Container übernommen. Dieser verwendet einen Katalog, um sich über die zur Verfügung stehenden Parts zu informiert. Der Katalog kann dazu beispielsweise eine genannte Assembly oder alle Assemblies eines Verzeichnisses nach Parts durchsuchen. Alternativ dazu können dem Katalog einzelne Parts auch direkt bekannt gemacht werden.

Modularisierung ist eine häufig genutzte Möglichkeit, um große Softwaresysteme wartbarer zu gestalten. Das seit 2010 in .NET 4 enthaltene Managed Extensibility Framework (MEF) unterstützt bei solchen Bestrebungen und kann darüber hinaus auch zum Umsetzen von Plug-in-Mechanismen herangezogen werden. Mit .NET 4.5 steht nun die zweite Version von MEF zur Verfügung. Die nachfolgenden Abschnitte zeigen, welche neuen Möglichkeiten diese Version mit sich bringt.

Um sicherzustellen, dass MEF importierende Komponenten nur mit geeigneten Exports verdrahtet, definiert der Entwickler für beide einen Vertrag, der aus der Angabe eines Typs und/oder eines Strings besteht. Nur wenn beide Seiten denselben Vertrag aufweisen, findet eine Verdrahtung statt.

API-basierte Konfiguration

In der ersten Version von MEF mussten Imports und Exports über Attribute gekennzeichnet werden. Ab MEF 2 kann der Entwickler diese Aufgabe auch über ein API, das das Definieren von Konventionen erlaubt, bewerkstelligen. Im Zuge dessen kann er zum Beispiel festlegen, dass MEF sämtliche Subtypen eines bestimmten Basistyps exportieren soll. Listing 1 und Listing 2 demonstrieren die Verwendung dieses API. Dazu beinhaltet Listing 1 ein Interface ILogger sowie eine Implementierung davon. Letztere soll exportiert werden. Daneben beinhaltet es die Klasse Calculator, deren Konstruktoren ein zu importierendes IEnumerable erwarten. Listing 2 zeigt den Einsatz des API, mit dem sämtliche Implementierungen von ILogger als Exports sowie einer der beiden Konstruktoren von Calculator als Importing Constructor definiert werden. Dreh- und Angelpunkt ist hierbei die Instanz der Klasse RegistrationBuilder, bei der die einzelnen Konventionen registriert werden. Die Methode ForTypesDerivedFrom nimmt Bezug auf sämtliche Implementierungen von ILogger; die Methode Export veranlasst – wie der Name vermuten lässt – den Export und SetCreationPolicy bestimmt, ob MEF den Export als Singleton, der von sämtlichen Imports zu teilen ist (CreationPolicy.Shared), einrichten soll, oder ob jeder Import eine eigene Instanz des Exports bekommt. Mit dem Lambdaausdruck, der an Export übergeben werden kann, legt der Aufrufer Details zum Export fest. Das betrachtete Beispiel legt auf diese Weise fest, dass als Vertrag der Typ ILogger zu verwenden ist.

Danach referenziert das Beispiel den Calculator mit ForType und exportiert ihn via Export. Daneben legt es mittels SelectConstructor den für den Import zu verwenden Konstruktor fest. Verzichtet der Entwickler auf diesen Aufruf, wird per Definition der Konstruktor mit den meisten Parametern herangezogen. Anschließend übergibt das Beispiel den RegistrationBuilder an den zu verwendenden Catalog und erzeugt damit auf die gewohnte Art und Weise einen CompositionContainer, über den es mittels GetExport eine Instanz von Calculator bezieht.

Listing 1
public abstract class ILogger {     public abstract void Log(string msg); }  public class ConsoleLogger: ILogger {     public override  void Log(string msg)     {         Console.WriteLine(DateTime.Now.ToString() + ": " + msg);     } }  public class Calculator {     private IEnumerable Logger { get; set; }      public Calculator(IEnumerable logger, string somethingElse)     {         this.Logger = logger;     }      public Calculator(IEnumerable logger)     {         this.Logger = logger;     }      public int Add(int a, int b)     {         var c = a + b;         Logger.ToList().ForEach(logger =>              logger.Log(string.Format("ADD: {0} + {1} = {2}", a, b, c)));          return c;     } }
Listing 2

var rb = new RegistrationBuilder();  rb.ForTypesDerivedFrom()     .Export(ec => ec.AsContractType())     .SetCreationPolicy(CreationPolicy.Shared);  rb.ForType().Export(); rb.ForType().SelectConstructor(             pib => new Calculator(pib.Import>()));  var catalog = new AssemblyCatalog(typeof(Program).Assembly, rb); var container = new CompositionContainer(catalog,                             CompositionOptions.DisableSilentRejection);  var calculator = container.GetExport().Value;  calculator.Add(2, 3);

[ header = Neuerungen im Managed Extensibility Framework (MEF) 2 – Teil 2 ]

Ein weiteres Beispiel für die Definition von Konventionen findet sich in Listing 3. Es legt mit ForTypesMatching fest, dass sämtliche Typen, deren Namen auf Helper enden, zu exportieren sind. Dazu nimmt ForTypesMatching einen Lambdaausdruck entgegen, der eine Instanz von Type auf einen booleschen Ausdruck abbildet. Dieser Ausdruck gibt an, ob der jeweilige Typ zu exportieren ist. Die Methode ImportProperty legt hingegen fest, dass MEF eine bestimmte Eigenschaft eines Typs als Ziel für Imports verwenden soll. Die Eigenschaft nimmt sie dabei als Lambdaausdruck entgegen. Weitere Details zum Import legt der Entwickler über einen optionalen zweiten Parameter fest. Auch hierbei handelt es sich um einen Lambdaausdruck. Dieser bildet eine Instanz von ImportBuilder (in Listing 3 als ib bezeichnet) auf die gewünschten Einstellungen ab. Mit ib.AsMany legt das betrachtete Beispiel fest, dass die jeweilige Eigenschaft mehrere Imports in Form einer Auflistung aufnehmen soll. Darüber hinaus exportiert es mittels AddMetadata Metadaten für die Typen ConsoleLogger und FileLogger. Die Klasse OtherCalculator, die als Importziel in diesem Beispiel dient, findet sich in Listing 4.

Listing 3
 var rb = new RegistrationBuilder();  rb.ForTypesMatching(t => t.Name.EndsWith("Helper")).Export();  rb.ForType().Export(); rb.ForType().ImportProperty(oc => oc.Logger,                                                           ib => ib.AsMany(true)); rb.ForType().ImportProperty(oc => oc.FormatHelper);  rb.ForType().Export(eb =>              eb.AddMetadata("minLevel", "debug").AsContractType() ); rb.ForType().Export(eb =>              eb.AddMetadata("minLevel", "info").AsContractType());  var catalog = new AssemblyCatalog(typeof(Program).Assembly, rb); var container = new CompositionContainer(catalog,                                CompositionOptions.DisableSilentRejection);  var calculator = container.GetExport().Value;              calculator.Add(2, 3);
Listing 4
public class OtherCalculator {     public IEnumerable>>                                                        Logger { get; set; }      public FormatHelper FormatHelper { get; set; }      [...] }
Leichtgewichtiges Programmiermodell

Neben dem klassischen Programmiermodell steht nun ein weiteres so genanntes leichtgewichtiges Programmiermodell zur Verfügung. Es kann über NuGet bezogen werden. Der Name des NuGet-Paketes lautet Microsoft.Composition. Während Microsoft das klassische Programmiermodell in erster Linie für Desktopanwendungen konzipiert hat, bietet sich das leichtgewichtige Programmiermodell für den Einsatz in Webanwendungen an, zumal es in Hinblick auf Durchsatz optimiert wurde. Darüber hinaus kann der Entwickler es auch innerhalb von Windows-8-Apps heranziehen.

Sowohl Attribute als auch Konventionen, die der Entwickler über ein API festlegt, erlauben hierbei das Definieren von Imports und Exports. Das Konzept von Catalog-Instanzen gibt es hier jedoch nicht. Stattdessen erzeugt der Entwickler eine Instanz von ContainerConfiguration (Listing 5). Diese konfiguriert er anschließend durch das Aufrufen von Methoden, die von dieser Instanz angeboten werden. Die Methode WithAssembly erlaubt zum Beispiel die Angabe einer Assembly, die das MEF nach Imports und Exports durchsuchen soll und dient somit als Ersatz für den aus dem klassischen Programmiermodell bekannten AssemblyCatalog.

Nachdem die erzeugte ContainerConfiguration konfiguriert wurde, kann der Entwickler über ihre Methode CreateContainer einen CompositionContainer, der in gewohnter Manier zu verwenden ist, erzeugen.

Listing 5
var config = new ContainerConfiguration()                             .WithAssembly(typeof(Program).Assembly);  var container = config.CreateContainer(); var calc = container.GetExport(); var c = calc.Add(2, 4); Console.WriteLine(c);

Zum Festlegen von Konventionen für das leichtgewichtige Programmiermodell verwendet der Entwickler eine Instanz von ConventionBuilder (Listing 6). Es handelt sich dabei um das Gegenstück der oben beschriebenen Klasse RegistrationBuilder, die das zuständige Produktteam im Zuge von MEF 2 für das klassische Programmiermodell eingeführt hat. Das betrachtete Beispiel definiert damit, dass sämtliche Klassen, die das Interface ILogger implementieren, unter Verwendung des Contract-Types ILogger zu exportieren sind.

Zur Erzeugung eines Containers ist auch bei dieser Vorgehensweise eine ContainerConfiguration zu instanziieren. Der Entwickler kann dieser den zu verwendenden ConventionBuilder über die Methode WithDefaultConventions bekannt machen.

Listing 6
 ConventionBuilder cb = new ConventionBuilder();  cb.ForTypesDerivedFrom().Export(ec => ec.AsContractType());  var configWithConventions = new ContainerConfiguration()                                     .WithDefaultConventions(cb)                                     .WithAssembly(typeof(Program).Assembly);  var container = config.CreateContainer();

[ header = Neuerungen im Managed Extensibility Framework (MEF) 2 – Teil 3 ]

Die nachfolgendende Auflistung zeigt einige Punkte, in denen sich das leichtgewichtige Programmiermodell vom klassischen unterscheidet:

  • Importierende und exportierende Member müssen öffentlich sein.
  • Das Importieren und Exportieren von Feldern ist nicht erlaubt. Stattdessen sollten Properties verwendet werden.
  • Parts werden nicht mehr standardmäßig geteilt. Sollten Parts geteilt werden, sind sie mit dem Attribut Shared zu versehen.
  • Das Interface IPartImportsSatisfiedNotification wird nicht unterstützt. Stattdessen kann eine Methode, die nach dem Abschluss der Importvorgänge aufzurufen ist, eingerichtet werden, indem diese mit OnImportsSatisfied annotiert wird.
  • Bei Metadata-Views muss es sich um konkrete Typen handeln. Interfaces werden hierfür nicht unterstützt.

Darüber hinaus befindet sich unter [1] eine ausführliche Tabelle, die kurz und prägnant darüber Auskunft gibt, welche Konstrukte von welchem Programmiermodell unterstützt werden.

Instanziierung von Imports mit ExportFactory beeinflussen

In der Vergangenheit konnte der Entwickler lediglich festlegen, ob das MEF einen Export als Singleton bereitstellen oder für jede importierende Stelle eine eigene Instanz des exportierten Typs erzeugen soll. Nun können die importierenden Komponenten auch selber entscheiden, wie viele Instanzen eines Imports sie benötigen. Dazu importieren sie den Typ ExportFactory, wobei der Typparameter T für den eigentlich zu importierenden Typ steht (Listing 7). Hat ein Objekt auf diese Weise eine ExportFactory erhalten, kann es mit CreateExport nach Belieben Instanzen von T erzeugen. Dabei gilt es zu beachten, dass diese Methode Instanzen von Lazy zurückliefert – die gewünschte Instanz muss der Entwickler somit über die Eigenschaft Value beziehen.

Listing 7
[Export(typeof(Manager))] public class Manager {     private ExportFactory factory;      [ImportingConstructor]     public Manager(ExportFactory factory)     {         this.factory = factory;     }      public void Start()     {         var task = this.factory.CreateExport().Value;         task.Exec();     } }
Beeinflussung des Instanziierungsverhaltens über Scopes

MEF 2 bietet nun auch die Möglichkeit, das Instanziierungsverhalten über so genannte „Composition-Scopes“ zu beeinflussen. Dieses Konzept erlaubt das Bereitstellen von Exports, die nur innerhalb ihres eigenen Wertebereichs (Scope) geteilt werden.

Listing 8 demonstriert dies, indem es zwei Instanzen von TypeCatalog erzeugt. Der erste bezieht sich auf den Typ Manager; der zweite auf die Typen BizTask, Repository, OtherRepository und Database. Anschließend zieht das betrachtete Beispiel diese Kataloge zum Einrichten einer CompositionScopeDefinition heran. Auf der obersten Ebene dieser CompositeScopeDefinition befindet sich der TypeCatalog mit dem Typ Manager. Den anderen TypeCatalog ordnet es diesem unter, indem es damit eine eigene CompositionScopeDefinition erzeugt und sie an den zweiten Parameter des Konstruktors der ersten CompositionScopeDefinition übergibt. Dabei ist zu beachten, dass eine CompositionScopeDefinition mehrere untergeordnete CompositionScopeDefinition-Instanzen haben kann. Diese werden gemeinsam als Array übergeben. Somit kann eine Baumstruktur mit Wertebereichen erzeugt werden.

Ein CompositionScopeDefinition repräsentiert auch einen Catalog, den der Entwickler zum Erzeugen eines CompositionContainers heranziehen kann. Das Instanziierungsverhalten eines solchen Containers wird durch Abbildung 1 dargestellt. Jeder vom Manager erzeugte BizTask befindet sich hierbei in einem eigenen Wertebereich; alle von diesem BizTask erhaltenen Imports befinden sich im selben Scope und werden geteilt. Aus diesem Grund existiert auch pro Wertebereich eine Instanz von Database, die von den beiden Repositories geteilt wird. Ohne den Einsatz von Scopes würde es hingegen für alle vier abgebildeten Repositories lediglich eine Database-Instanz geben bzw. auf Wunsch des Entwicklers eine Instanz pro Repository.

Listing 8
var managerLevel = new TypeCatalog(typeof(Manager)); var taskLevel = new TypeCatalog(     typeof(BizTask),     typeof(Repository),     typeof(OtherRepository),     typeof(Database));  var cat = new CompositionScopeDefinition(                 managerLevel,                 new[] { new CompositionScopeDefinition(taskLevel, null) });  var container = new CompositionContainer(cat);  var manager = container.GetExport().Value;

Scopes

Fazit

Durch die Möglichkeit, das MEF über ein API zu konfigurieren, wird die Abhängigkeit zwischen den einzelnen Komponenten und dem MEF aufgehoben. Dies gibt dem Entwickler die Möglichkeit, das MEF bei Bedarf durch ein anderes Framework zu ersetzen. Viel wichtiger ist aber die Tatsache, dass diese Komponenten nun auch in Softwareschichten, die nicht auf das MEF referenzieren, verwendet werden können.

Die Tatsache, dass es nun eine Spielart vom MEF gibt, die für Webszenarien optimiert wurde und innerhalb von Windows-8-Apps genutzt werden kann, dürfte vielen Entwicklern sehr entgegenkommen. Hierbei stellt sich jedoch leider auch die Frage, ob es wirklich notwendig war, diese Unterstützung durch ein eigenständiges Programmiermodell zu realisieren, zumal man auch mit wenig Fantasie bereits bei der Konzeptionierung der ersten Version von MEF zur Erkenntnis kommen konnte, dass Webanwendungen früher oder später unterstützt werden müssen.

Unsere Redaktion empfiehlt:

Relevante Beiträge

Meinungen zu diesem Beitrag

X
- Gib Deinen Standort ein -
- or -