Castle Project (http://www.castleproject.org) bietet mit seinem MicroKernel eine sehr gute kostenlose Möglichkeit für den einfachen Einsatz von einem InversionOfControl Container und dem Einsatz von Dependency Injection.

Da ich hier in erster Linie die Anwendung demonstrieren will, gehe ich nicht näher auf eine Erklärung des Begriffs „Dependency Injection“ ein. Ich verweise daher auf den Artikel in Wikipedia und zitiere daraus nur einen Satz:

Dependency Injection ist eine Anwendung des Prinzips der Inversion of Control (IoC), bezieht sich aber nur auf die Erzeugung und Initialisierung von Objekten. Sie kann als Verallgemeinerung der Fabrikmethoden verstanden werden. Die Funktionalität bleibt trotz dieser Kontrollumkehr als Einfügung enthalten. Dadurch ist es einfach möglich, Abhängigkeiten zu erkennen.

Quelle: **http://de.wikipedia.org/wiki/Dependency_Injection* *

Da es nur wenige vollständige Tutorials gibt, die den vollständigen Einsatz von Dependency Injection demonstrieren, hab ich beschlossen ein solches hier zusammen zustellen.

Neben dem reinen Einsatz will ich hier auch ein bisschen auf eine mögliche Projekt-Struktur eingehen. Mir persönlich fällt das Verständnis leichter, wenn ich eine sinnvolle Projekt-Struktur vor mir habe.

Funktionelles Ziel des Beispiel-Projekts

Ziel der Anwendung soll es sein, dass man über verschiedene Daten aus einer Datenbank ein Statistik-Objekt erstellt und dieses dann in der Anwendung weiterverarbeitet. Dazu werden die Tabellen Customer, Order und Product verwendet. Diese werden über Repositories abgefragt. Ein Service generiert dann das entsprechende Statistik-Objekt und schickt dieses an die Anwendung zurück.

Ich werde die funktionellen Logiken der einzelnen Objekte nicht weiter ausformulieren, da diese nichts mit der aktuellen Thematik zu tun haben.

Allgemeiner Projekt-Aufbau

Das Ziel bei Objektorientierter Programmierung sollte immer sein, dass man verschiedene Aufgaben weitestgehen voneinander trennt. Bei dieser Trennung gehe ich so weit, dass die verschiedenen Aufgaben sogar in unterschiedlichen Assemblies untergebracht werden.

So entstehen folgende Assemblies:

IoCSample:
Das ist das Hauptprogramm

**IoCSample.Core:
**In diesem Projekt befinden sich allgemeine Funktionen, die in allen Teilprojekten verwendet werden sollen und vorallem die gesamte API (Interfaces) für den Zugriff auf die Repositories und Services.

**IoCSample.Domain:
**Hier findet man alle reinen POCOs (Plain Old CLR Objects), die in der Anwendung benötigt warden. Also das reine Customer-Objekt, Product-Objekt usw. In dieser Assembly wird keinerlei Logik implementiert außer vielleicht Standardmethoden wie ToString(), GetHashCode() oder Equals().

**IoCSamle.Data:
**In dieser Assembly wird der Datenbank-Zugriff stattfinden. In meinem Beispiel wird keine echte Datenbank verwendet, daher werden hier lediglich die Dummy-DataObjects erzeugt und je nach Bedarf zurückgegeben.

**IoCSample.Services:
**In diesem Projekt wird die Hauptlogik implementiert. Das Erstellen der Statistik in meinem Beispiel. Von hier aus werden alle relevanten Daten geholt und entsprechend aufbereitet.

Von den Projekt-Verweisen her sieht das Ganze folgendermaßen aus: Sowohl das Core-Project als auch das Domain-Project werden von allen anderen Assemblies verwendet. Das Haupt-Projekt (IoCSample) aber kennt auch wirklich nur diese beiden Assemblies (Es gibt auch den Ansatz, dass man die Domain-Objekte in die CoreAssembly verlagert). Ebenso kennen sich die Assemblies Data und Services nicht gegenseitig. Die Kommunikation erfolgt ausschließlich über die Interfaces, die im Core-Project definiert wurden. Dies hat außerdem den Vorteil, dass man zum Beispiel einfach die Data-Assembly austauschen kann, wenn man zum Beispiel auf eine andere Datenbank gehen will oder die Daten statt aus einer Datenbank von einem WebService oder ähnlichem holen will.

Realisierung ohne Dependency Injection

Um die Vorteile zu erläutern will ich hier erst einmal zeigen, wie man eine Realisierung im Normalfall durchführen würde.

Da der Service intern die Repositories für den Datenzugriff benötigt, müsste man diese erst erzeugen und dann dem Service zuweisen. Erst dann kann man den Service vollständig benutzen.

static void Main(string[] args)
{
	Guid customerId = Guid.NewGuid();
	ICustomerRepository repCustomer = new CustomerRepository();
	IProductRepository repProduct = new ProductRepository();
	IOrderRepository repOrder = new OrderRepository();
	IStatisticRepository service = new StatisticRepository(
		repCustomer, repOrder, repProduct);

	Statistik = service.CreateStatistic(customerId);
}

Hier sieht man auch gleich das erste Problem. Dadurch, dass die Repository-Objekte erst erzeugt werden müssen, muss dem Haupt-Projekt auch die entsprechende Assembly bekannt gemacht werden. Weiter oben haben wir aber gesagt, dass das Haupt-Projekt lediglich die Assemblies Core und Domain kennen soll. Also wäre dieser Ansatz ohnehin nicht einsetzbar. Man könnte natürlich auch mit einer entsprechenden Factory in der Core-Assembly arbeiten, aber damit müsste diese Assembly dann die Repositories wieder kennen, was auch nicht der Fall ist. Für eine komplette Trennung ist also nur der Weg über Reflection möglich, was unter Umständen sehr viel Aufwand bedeuten kann.

Wenn man sich den Quellcode im beiliegenden Projekt mal anschaut, sieht man außerdem, dass eine Erstellung der Objekte ohnehin nicht möglich wäre, da ich die Repositories und auch den Service als *internal *deklariert habe. Das ist zwar nicht unbedingt nötig, aber damit kann ich sicherstellen, dass meine Erstellungslogik nicht umgangen wird und man gezwungen ist mit den Interfaces zu arbeiten.

Realisierung mit Dependency Injection

Wenden wir uns nun dem eigentlichen Ziel des Tutorials zu und nehmen den Weg über den MicroKernel von CastleProject.

Der erste Schritt dafür ist das Anlegen einer Wrapper-Klasse für den Zugriff auf die IoC. Diese Klasse (IoC) soll allgemein gültig sein und sollte daher in der Core-Assembly angelegt werden. Es wäre auch denkbar, dass man den Wrapper im Haupt-Projekt anlegt um zum Beispiel zu gewährleisten, dass die Objekte lediglich im Haupt-Projekt verwendet werden können. Das ist alles eigene Ermessenssache.

Der Wrapper benötigt eigentlich nur zwei Methoden. Eine zum Konfigurieren des Conainers und eine zum Resolven der Objekte. In meinem Beispiel-Projekt sind noch weitere Überladungen dazu enthalten, weil man immer wieder mal eine andere Art brauchen kann.

public static class IoC
{
	private static IWindsorContainer _container; 

	public static void Configure()
	{
		_container = new WindsorContainer(
		new XmlInterpreter(
			new ConfigResource("castle")));
	} 

	public static TService Resolve<TService>()
	{
		return _container.Resolve<TService>();
	}
}

In diesem Fall liest der Wrapper die Konfiguration aus der app.config der aktuellen Anwendung.

Dies ist auch der nächste Schritt bei der Umstellung auf Dependency Injection. In der app.config werden die Verknüpfungen zwischen den interfaces (service) und den echten Klassen (type) festgelegt. Diese Komponenten-Konfigurationen werden dann vom Windsor-Container verwaltet und je nachdem welche Anfrage er bekommt, holt er das entsprechende benötigte Objekt und bereitet dieses für die Bearbeitung vor.

<?xml version="1.0" encoding="utf-8" ?>
<configuration>
   <configSections>
      <section name="castle"
         type="Castle.Windsor.Configuration.AppDomain.CastleSectionHandler,
         Castle.Windsor" />
   </configSections>
   <castle>
      <components>
         <!-- REPOSITORIES -->
         <component
            id="CustomerRepository"
            service="IoCSample.Core.Repositories.ICustomerRepository, IoCSample.Core"
            type="IoCSample.Data.CustomerRepository, IoCSample.Data" />
         <component
            id="OrderRepository"
            service="IoCSample.Core.Repositories.IOrderRepository, IoCSample.Core"
            type="IoCSample.Data.OrderRepository, IoCSample.Data" />
         <component
            id="ProductRepository"
            service="IoCSample.Core.Repositories.IProductRepository, IoCSample.Core"
            type="IoCSample.Data.ProductRepository, IoCSample.Data" />
         <!-- SERVICES -->
         <component
            id="StatisticService"
            service="IoCSample.Core.Services.IStatisticService, IoCSample.Core"
            type="IoCSample.Services.StatisticService, IoCSample.Services" />
      </components>
   </castle>
</configuration>

Das war dann auch schon alles was man für die erste Anwendung benötigt. Durch die Konfiguration in der app.config sieht man auch schön, welche weiteren Möglichkeiten dieses Konzept bietet. Man kann nämlich dadurch ganz einfach auf eine andere Assembly verweisen, wenn es gerade nötig sein sollte. Auch für die Mock-Entwicklung kann dieses Konzept dann sehr interessant sein, wenn man statt echten Repositories irgendwelche Dummies verwendet um nur die Services zu testen.

Da wie oben erwähnt die Service-Assembly und die Data-Assembly im Hauptprojekt nicht bekannt sind, müssen wir zusätzlich noch ein PostBuild-Ereignis einrichten, damit beim Kompilieren die beiden Assemblies noch in das Bin-Verzeichnis kopiert werden. Wenn IoC beim Konfigurieren eine Assembly nicht findet, wird ein entsprechender Fehler ausgegeben. Dieser Punkt wird leider immer sehr gerne vergessen. Daher sollte man sich das Implementieren dieser Ereignisse möglichst frühzeitig gleich angewöhnen.

Kommen wir nun wieder zurück zum Haupt-Programm und schauen wir uns nun an, wie das gleiche Erstellen einer Statistik nun aussehen würde.

static void Main(string[] args)
{
	IoC.Configure();
	Guid customerId = Guid.NewGuid();
	Statistic statistic =
		IoC.Resolve<IStatisticService>().CreateStatistic(customerId);
}

Wie man sieht, ist der Aufruf jetzt sehr viel kürzer und man muss sich nicht mehr darum kümmern, welche Objekte für den Service intern notwendig sind.

Das Konzept ist auch sehr wichtig, wenn verschiedene Personen an einem Projekt arbeiten. So kann man die Leute zum Beispiel so einteilen, dass sich einer nur um die Oberfläche kümmert, der zweite schreibt nur die Logik in den Services und der dritte kümmert sich um das Heranschaffen der Daten. Eine gute Kommunikation zwischen den einzelnen Instanzen wird natürlich vorausgesetzt.

Kurze Erläuterung zum Schluß

Zum Abschluß will ich noch kurz erläutern, warum das funktioniert:

Wir holen über den IoC-Container das Service-Objekt. Der MicroKernel kontrolliert den Konstruktur des angeforderten Objekts und erkennt, welche zusätzlichen Objekte für diesen Service noch notwendig sind. Da er diese Objekte über die Konfiguration auch kennt, werden erst diese erzeugt und der Konstruktor des Services dann entsprechend damit gefüttert. Sollte ein Konstruktur ein Objekt benötigen, welches nicht in der app.config eingetragen ist, wird ein entsprechender Fehler geworfen.

Diese Kette kann man not um sehr viele weitere Stufen fortsetzen. Wenn zum Beispiel die Repositories noch ein IDatabase-Objekt benötigen, kann der MicroKernel auch noch dieses Objekt automatisch erzeugen usw.

Letzten Endes ist für die Anwendung von Dependency Injection erst einmal mehr Vorarbeit notwendig als wenn man einfach drauf los programmiert. Dafür kann man aber seine Anwendungen viel klarer trennen, was letzten Endes zu einer besseren Struktur des Codes führt und damit auch die Fehler-Findung erheblich erleichtert. Ein einheitliches Design der Anwendung reduziert außerdem die Einarbeitungszeit für neue Kollegen.

Weitere Möglichkeiten

Da ich zur Zeit in einem Projekt ebenfalls mit dem IoC-Container arbeite, diesen aber lediglich zur Entkopplung der Assemblies verwende, möchte ich hier noch auf eine weitere Möglichkeit hinweisen.

Die Anforderung dieser Anwendung ist es, dass Daten sowohl aus einer Single-User-Datenbank als auch aus einer Multi-User-Datenbank geholt werden sollen. Das heißt, ich habe für jedes Interface zwei Repositories zur Verfügung die je nach Situation verwendet werden sollen.

Um dies zu Realisieren hab ich den IoC-Wrapper dahingehend erweitert, dass ich ihm den Namen der zu verwendenden Assembly zuweise und beim Erstellen der Objekt überprüfe, aus welcher Assembly diese Objekt kommen würden.

Der Code sieht folgendermaßen aus:

public static TService Resolve<TService>()
{
	TService[] services = _container.ResolveAll<TService>();
	if (services.Length == 1)
		return services[0]; 

	foreach (TService service in services)
	{
		if (service.GetType().Assembly.GetName().Name.Equals(_assemblyName))
			return service;
	} 

	if (services.Length == 0)
		// damit die richtige Fehlermeldung geworfen wird
		return _container.Resolve<TService>(); 

	// bis jetzt noch nichts passendes gefunden, dann nehmen wir
	// eben den ersten vorhandenen
	return services[0];
}

Es werden also alle Komponenten geholt, die zum jeweiligen Interface passen und dann wird entschieden, welche dieser Komponenten an die Anwendung zurückgegeben wird.




Weitere Informationen

http://www.castleproject.org/container/gettingstarted/index.html
http://msdn.microsoft.com/en-us/magazine/cc163739.aspx
http://www.codefest.at/post/2009/11/27/Design-Patterns-Teil-1-e28093-Inversion-of-Control-Dependency-Injection.aspx