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