Managed Extensibility Framework

 Von Damir Dobric

.NET 4.0, Composition, Extensibility und Plug-In Model

Das neue .NET 4.0 wird eine Bibliothek mit der standardisierten Infrastruktur für die Entwicklung von erweiterbaren Anwendungen enthalten. Jeder Entwickler der bereits eine erweiterbare Anwendung implementiert hat weiß, dass die Implementierung einer solchen Infrastruktur notwendig und leider sehr mühsam ist. Dieser Artikel beschreibt das .NET 4.0 Managed Extensibility Framework, das die Entwicklung von erweiterbaren Anwendungen vereinfachen soll.

Gutes Design

Eine gute Anwendung entspricht gewollt oder nicht einigen in der Praxis Bewährten Prinzipien.

Manche Architekten und erfahrene Entwickler versuchen bewusst, diese bewährten Prinzipien (Patterns) in Ihren Anwendungen umzusetzen.

Manchmal programmieren die erfahrene Entwickler sogar unbewusst nach diesen Prinzipen. Einige Patterns die  Erweiterbarkeit einer Anwendung bestimmen, sind unter anderem: Open Closed Principal , [1], Liskov Substitution Principal [2], Dependency of Inversion Control [3] und Design By Contract [4].

Dieser Artikel konzentriert sich nicht auf OO-Patterns, dennoch ist es wichtig zu erwähnen, dass  eine Anwendung die auf Basis einiger dieser Patterns implementiert ist, wahrscheinlich hochqualitativ ist.

Interessanterweise zeigt uns die Erfahrung, dass Anwendungen die mit o.g. Prinzipen untermauert sind in der Regel relativ leicht erweiterbar sind.

Zum Beispiel, setzt das Open/Closed Prinzip auf die Erweiterung der Funktionalität ohne Änderung von Kern der Anwendung der die Basis-Funktionalität darstellt. Ähnliches Beispiel ist Substitution Prinzip. Inversion of Control und Design By Contract setsehen den Umgang mit Abstrakten Spezifikationen (Schnittstellen) vor.

Allen, die die oben genannten Pattern nicht kennen, sei die Lektüre der angegebenen Artikel [1-4] ans Herz gelegt.

Im Allgemeinen eine Anwendung die nach diesen Regeln konzipiert ist, besteht aus einigen Modulen (also modular), die austauschbar sein sollten.

Folgendes Beispiel soll das erläutern:

ComponentInstance inst = new ComponentInstance ();

Im diesem Beispiel ist es nicht möglich die Komponente ComponentInstance auszutauschen ohne den Source Code zu ändern. Mit solcher Vorgehenseise, kann man keine modularen Anwendungen implementieren.

Unabhängig von Pattern sollte man grundsätzlich wie folgt vorgehen:

IComponent proxy = new SomeFactory.GetComponentInstance (...);

Dieses Beispiel folgt dem Design by Contract Pattern und bildet die Grundlage für die Implementierung der Patterns Inversion of Control, Substitute Prinzip usw.

Im Grunde geht es darum den Operator new zu eliminieren.

Die Instanz proxy Liefert ein Objekt zurück,welches das nach IComponent-Vertrag definiert ist. Welches Objekt (Implementierung) sich dahinter Verbirgt, ist uninteressant da diese Komponente gegen eine andere Implementierung ausgetauscht werden kann.. In diesem Konkreten Fall, könnten beispielsweise die Komponenten Component1 und Component2 die Schnittstelle IComponent implementieren.

Modularität und Erweiterbarkeit

Das letzte Beispiel  ermöglicht die Verwendung von zwei Komponenten die dieselbe Schnittstelle implementieren. Je nach Anforderung kann in einer Anwendung eine der Komponenten verwendet werden und später durch eine andere ausgetauscht werden. Es gibt ebenso Beispiele in denen beide oder sogar mehrere Komponenten gleichzeitig verwendet werden.

Wie man sieht, bietet diese Architektur der Anwendung die größt mögliche Flexibilität.

Soweit die Theorie. Eine Kleinigkeit bleibt noch offen:. Das Ganze funktioniert nur mit Hilfe von SomeFactory. Hinter diese Klasse verbirgt sich meistens eine Art von Framework. Mit anderen Worten eine Infrastruktur, die den Aufbau einer modularen Anwendung ermöglicht..

Für diese Aufgabe existieren bereits einige Frameworks [6]. Manchmal implementiert man diese auch selbst. Ähnliche Beispiele gibt es z.B. in der Workflow Foundation (Services) aber auch in der Windows Communication Foundation (Behaviors).  Bermerkenswerter handelt es sich bei diesen zwei Beispielen um zwei verschiedene Implementierungen des selben Herstellers für das gleiche Problem. Es gibt zahlreiche solche Beispiele, die im .NET Framework 4.0 unter dem Namen „Managed Extensibility Framwork“ (MEF) integriert werden. MEF ist momentan im CodePlex [5] zu finden. Es steht aber schon fest, das die Bibliothek Bestandteil von .NET 4.0 wird.

MEF

MEF ist eine Bibliothek die das Problem der Erweiterbarkeit zur Laufzeit (Runtime Extinsibility) löst. Sie vereinfacht die Implementierung von Erweiterbaren Anwendungen und bietet Ermittlung von Typen, Erzeugung von Instanzen und Composition Fähigkeiten an.

Die Abbildung 1 zeigt vereinfacht die Architektur.

Abb1_MEF_Diagram.gif

Abbildung 1: vereinfacht die Architektur

Die wichtigste Module im Core der Bibliothek sind Catalog und CompositionContainer. Das Catalog kontrolliert das Laden von Komponenten, während das CompositionContainer die Instanzen erzeugt und diese an die entsprechenden Variablen bindet.

Parts sind die Objekte die vom Type Export oder Import sein können. Die „Exports“ sind im beschriebenen Beispiel die Komponenten, die geladen und instanziiert werden sollen. Die „Imports“ sind die Variablen an die die Instanzen von der Komponenten gebunden werden sollen.

Zum Beispiel wäre die bereits erwähnte Komponente Component1 in der MEF-Anwendung ein Export.

Die Variable proxy vom Type IComponent die die Instanz dieser Komponente enthalten soll, wäre ein „Import“.

Um eine Komponente als Part zu definieren genügt das entsprechende Attribut;

[Export]
class Component1 : IComponent{}
. . .
[Import]
IComponent proxy

Das schöne am MEF ist, dass die Instanziierung automatisch mit Hilfe von Catalog und Container erfolgt.

Listing 1 zeigt ein MEF-Hello World Beispiel. Der Code in der Methode  Run() zeigt, wie das MEF-Framework gestartet wird (Mehr davon etwas später). In diesem Beispiel ist es lediglich wichtig, dass wenn die Methode-Run() fertig mit der Ausführung ist, enthält die Variable SingleObject die Instanz von Klasse SampleComponent1.

Listing 1 MEF - Hello World

using System;
using System.ComponentModel.Composition;
using MefSample.Components.Sample1;
using System.ComponentModel.Composition.Hosting;
namespace MefSample.Samples
{
    [Export("http://daenet.eu/mef/Sample1")]
    public class SampleComponent2 : ISample1, INotifyImportSatisfaction
    {}
    public class HelloWorldSample1
    {
        #region Sample1
        [Import("http://daenet.eu/mef/Sample1")]
        public ISample1 SingleObject { get; set; }
    public void Run()
        {
            AssemblyCatalog catalog = new AssemblyCatalog(System.Reflection.Assembly.GetExecutingAssembly());
            var container = new CompositionContainer(catalog);
            var batch = new CompositionBatch();
            batch.AddPart(this);
            container.Compose(batch);
            Console.WriteLine(SingleObject.ToString());
        }
        #endregion
    }
}

In diesem trivialen Beispiel scheint MEF möglicherweise nicht besonders hilfreich zu sein. Man sollte wissen, dass in einer realen Anwendung sehr viele Variablen wie SingleObject (Imports) überall im Code verteilt sind. Darüberhinaus die Kompatiblen Exports-Komponenten ( wie SampleComponent1)  könnten ebenso quer durch viele Assemblies verteilt werden.

Zumindest die Anzahl von Codezeilen, die man durch Die Verwendung von MEF spart, ist nicht zu unterschätzen.

Noch interessanter ist es, eine MEF-Anwendung konform zu o.g. Patterns. Damit ist es nicht gemeint, dass MEF alle Patterns abdeckt, sondern vielen einfach entspricht.

Katalog

Ein Katalog im MEF bestimmt die Art und Weise wie die Komponenten geladen werden und von wo sie geladen werden. Wichtig ist die Tatsache, dass die Komponenten entweder in einer gemeinsamen Assembly enthalten sind, in einer oder mehreren Assemblies statisch referenziert sind oder sogar in einer oder mehreren Assemblies dynamisch geladen werden. Um alle Möglichkeiten zu unterstützen bietet MEF einige Katalog-Klassen an. Jede dieser Klassen ist für das Laden von Komponenten zuständig.

Folgende Kataloge werden momentan unterstützt:

  • AssemblyCatalog
  • TypeCatalog
  • DirectoryCatalog
  • AgregatingCatalog

AssemblyCatalog hat die Fähigkeit die Parts (Imports und Exports) aus einer angegebenen Assembly zu laden.

TypeCatalog ermöglicht Composition von explizit gegebenen Typen.

DirectoryCatalog lädt die Assemblies (Parts in Assemblies) aus dem gegebenen Verzeichnis. Darüber hinaus reagiert er (wenn spezifiziert)  auf Veränderungen wie z.B. das hinzufügen neuer Assemblies im angegebenen Verzeichnis.

AggrgationCatalog kombiniert unterschiedliche Kataloge.

Listing 2 zeigt die Anwendung von allen Katalogen.

Listing 2 MEF – Anwendung von Katalogen

string path = @"..\..\..\MefComponents\bin\debug";

var catalog = new AggregateCatalog();

// Types from assemblies in folder: "..\..\..\MefComponents\bin\debug"
catalog.Catalogs.Add(new DirectoryCatalog(path, false));

// Add sample component.
catalog.Catalogs.Add(new TypeCatalog(Type.GetType(typeof(SampleComponent1).AssemblyQualifiedName)));

// Add a class with private constructor.
catalog.Catalogs.Add(new TypeCatalog(typeof(ClassWithPrivateConstructor)));

// Load parts from some assembly.
catalog.Catalogs.Add(new AssemblyCatalog(Assembly.Load("SomeAsssembly.dll")));

// Make container which will compose all catalogs
m_Container = new CompositionContainer(catalog);

var batch = new CompositionBatch();
batch.AddPart(this);

m_Container.Compose(batch);

Composing Prozess

Listing1 und Listing2 haben sog. CompositionContener und CompositionBatch verwendet.

CompositionContainer bündelt alle Kataloge zusammen und startet einen sog. Composing-Prozess, der in allen Katalogen nach Parts sucht und die „Exports“ an „Imports“ bindet. Nächstes Beispiel zeigt wie der Container mit einem Catalog erzeugt wird:

var catalog = new
AggregateCatalog();
. . .
CompositionContainer container = new CompositionContainer(catalog);

Neben Katalogen spielt auch sog. CompositionBatch Klasse eine wichtige Rolle im Composing- Prozess. Da MEF die Composition während der Laufzeit unterstützt, muss es eine Möglichkeit geben, die Parts zur Laufzeit zu entfernen oder neue hinzufügen. Die Klasse CompositionBatch ist für diese Aufgabe zuständig. Folgendes Beispiel zeigt wie das geht:

var batch = new CompositionBatch();
batch.AddPart(this);
batch.RemovePart(obj1)
container.Compose(batch);

Binden von Komponenten

Bisher haben wir gesehen, wie die MEF-Infrastruktur aussieht und wie man Parts miteinander verbindet.

Die Bindung zwischen Parts ist vielfältig und sehr mächtig. Im einfachsten Fall wird ein Export-Part an einen Import-Part gebunden. Wie Im Beispiel an der Bindung von . Component1 an die Variable Proxy schon gezeigt. Das funktioniert so lange die Komponente ( Component1 ) vom gleichem Type wie die Variable proxy ist. Dabei ist es nicht erlaubt mehrere Export-Parts an eine Variable zu Composen. Zum Beispiel könnte es sein, dass zwei Kataloge jeweils ein Export-Part vom erwarteten type IComponent implementieren. Um solche Fälle besser kontrollieren zu können, ist es möglich und empfehlenswert eine Art von Vertrag (Contract) zu verwenden:

[Export("Part1")]
public class SampleComponent1 : ISample1
[Export("Part2")]
public class SampleComponent1 : ISample1

Die Importseite würde analog wie folgt aussehen:

[Import("Part1")]
public ISample1 SingleObject { get; set; }
[Import("Part2")]
public ISample1 SingleObject { get; set; }

Häufig ist es notwendig mehrere Parts (Z.B. AddIns) in eine Liste laden:

[Import("AddIn")]
public IEnumerable<object> m_SampleObjects { get; set; }

Die Export-Parts (AddIns) würden wie üblich einfach mit dem Export Attribut und dem „AddIn“ Vertrag markiert.

In etwas komplexeren Fällen, möchte man vielleicht die Bindung nicht automatisch ablaufen lassen. Dies ist dann der Fall, wenn man durch mehrere Bindungen eine Rekursion verursachen würde.. In solchen Fällen verwendet man Lazzy-Load:

[Import]private ExportCollection<IComponent1> m_LazzyList { get; set; }
[Import]private Export<IComponent2> m_LazzyObject { get; set; }

Unterstützung von Metadaten

Neben der einfacher Bindung von Parts, gibt es die Möglichkeit die Parts mit den Metadaten zu versehen. Die Klasse ComponentWithMetadata im nächsten Beispiel exportiert untzpisiert  zwei Eigenschaften Position und Priority:

[Export("http://daenet.eu/mef/MetadataSample")]
[ExportMetadata("Position", "Left")]
[ExportMetadata("Priority", 1)]
class ComponentWithMetadata{}

Listing 3 zeigt wie diese Komponente gebunden wird und wie die Metadaten ausgelesen werden.

Listing 3

public class MetadataSample
{
    [Import("http://daenet.eu/mef/MetadataSample")]
    [ImportRequiredMetadata("Position")]
    [ImportRequiredMetadata("Priority")]
    private ExportCollection<IMetadata> m_ObjectsWithMetadata { get; set; }
    public void Run()
    {
        AssemblyCatalog catalog = new AssemblyCatalog(System.Reflection.Assembly.GetExecutingAssembly());
        var container = new CompositionContainer(catalog);
        var batch = new CompositionBatch();
        batch.AddPart(this);
        container.Compose(batch);
        foreach(Export export in m_ObjectsWithMetadata)
        {
            Console.WriteLine(export.Metadata["Position"]);
            Console.WriteLine(export.Metadata["Priority"]);
            IMetadata obj = export.GetExportedObject() as IMetadata;
            obj.Trace();
        }
    }
}

In einer komplexen Anwendung bietet sich mehr an die typisierten Metadaten zu verwenden.

In diesem Falle muss eine Attribut-Klasse implementiert werden, die die Metadaten repräsentiert. Die typisierte Variante des vorherigen Beispiels würde damit wie folgt aussehen:

[MetadataAttribute]
[AttributeUsage(AttributeTargets.Class)]
public class DockingCapabilitiesAttribute : Attribute
{
    public Position Position { get; set; }
    public int Priority{ get; set; }
}

Der Type Position ist eine Enumeration, die hier aus Platzgründen nicht dargestellt ist.

Ein Export-Part würde man wie folgt definieren:

[Export("http://daenet.eu/mef/StronglyTypedMetadataSample")]
[DockingCapabilities(Position = Position.Button, Priority = 1)]
class ComponentWithTypeMetadata : IMetadata

Beim Importieren ist der einzige Unterschied im Vergleich zum Listing 3, die Deklaration der Import-Variable:

[Import("http://daenet.eu/mef/StronglyTypedMetadataSample")]
private ExportCollection<IMetadata, DockingCapabilitiesAttribute> m_ObjectsWithMetadata { get; set; }

Fazit

Es ist sehr erfreulich, dass eine solche Bibliothek endlich standardisiert wurde. Dadurch wird es gewähreistet, dass der Life-Cycle der Bibliothek viel länger wird als es im Kontext von Patterns and Practices üblich ist.

MEF wird von einigen Teams von Microsoft bereits eingesetzt und es ist zu erwarten, dass bald viele Microsoft Produkte diese Infrastruktur als die Basis für Third-Party-AddIns verwenden. Das prominentesten Beispiele werden Expression Blend, Power Shell und Visual Studio 2010 sein.

[1] Open Closed Principal

http://www.objectmentor.com/resources/articles/ocp.pdf

[2] Liskov Substitution Principal

http://www.objectmentor.com/resources/articles/lsp.pdf

[3] Dependency of Inversion Principal

http://www.objectmentor.com/resources/articles/dip.pdf

[4] Design By Contract Principal

http://en.wikipedia.org/wiki/Design_by_contract

[5] Managed Extensibility Framework im CodePlex

http://www.codeplex.com/MEF

[6] Häufig mit MEF verwechselt: Windsor Container im Castle Projekt

http://www.castleproject.org/container/index.html

[7] Beispiel zu diesem Artikel

http :// developers . de / media / p /4006. aspx