Was Dependency Injection (DI) ist, möchte ich an dieser Stelle nicht mehr erläutern. Dass es jeder braucht auch nicht :) Ninject ist ein kostenloses DI-Framework auf Grundlage des .NET Frameworks.
Im Gegensatz zu dem bekannten Spring.NET wird die Konfiguration bei Ninject nicht über XML Files sondern direkt im Code durchgeführt.
Die Vorteile einer Konfiguration im CLR Code liegen klar auf der Hand
- Robustheit (Compile-Time-Check der Konfiguration)
- Performance (Verwendung von Code Generation im Gegensatz zu Reflection)
- Einfachheit (leicht erlernbar)
- Lesbarkeit (Mappings sind in der Hauptanwendungssprache geschrieben)
Ein einfaches Beispiel
Um Ninject verwenden zu können muss eine Referenz zur Assembly Ninject.Core.dll hinzugefügt werden. Im Anschluss daran muss der Namespace Ninject.Core noch durch eine entsprechende using Anweisung verfügbar gemacht werden
using Ninject.Core;
Um eine einfaches Ninject Beispiel zu erstellen benötigt man zunächst einige Contracts, die man später “injecten” möchte.
#region interfaces
// interface for addresses
public interface IAddress
{
String PoBox { get; set; }
bool HasPoBox();
void PrintPoBox();
}
//interfaces for media devices
public interface IMediaDevice
{
void PrintAddress();
}
#endregion
Natürlich auch noch ein paar Implementierungen der Interfaces.
#region Implementations
// example implementation for IMediaDevice
public class WindowsPhone : IMediaDevice
{
// custom property...
public String IpAddress { get; set; }
// interface method
public void PrintAddress()
{
Console.WriteLine(
String.Format("Your Windows Phone IP address is {0}"
, this.IpAddress));
}
}
public class IPhone : IMediaDevice
{
public void PrintAddress()
{
Console.WriteLine(
@"Your IPhone doesn't need an IP address,
it is powerfull as Chuck Norris!
So if you try to reach it,
it will give you a roundhouse kick :D");
}
}
// example implementation for IAddress
public class NationalAddress : IAddress
{
public String PoBox { get; set; }
public bool HasPoBox()
{
return !String.IsNullOrEmpty(this.PoBox);
}
public void PrintPoBox()
{
if (HasPoBox())
{
Console.WriteLine(String.Format("PoBox is specified to '{0}'", this.PoBox));
return;
}
Console.ForegroundColor = ConsoleColor.Red;
Console.WriteLine("PoBox is not specified");
Console.ResetColor();
}
}
public class InternationalAddress : IAddress
{
public string PoBox
{
get;
set;
}
public bool HasPoBox()
{
return false;
}
public void PrintPoBox()
{
Console.ForegroundColor = ConsoleColor.Red;
Console.WriteLine(
@"PoBoxes aren't supported for international addresses");
Console.ResetColor();
}
}
#endregion
Bis hierin ist alles noch Standard, noch ist von Ninject nichts zu sehen. Was nun noch an Datenmodellen fehlt ist ein Consumer, jemand der mit den Interfaces (bzw. später mit Instanzen der Implementierungen) arbeitet.
#region consumer...
// simple class
public class Customer
{
public IAddress MainAddress { get; set; }
public int Id { get; set; }
//property injection
[Inject]
public IMediaDevice MediaDevice { get; set; }
//Constructor Injection
[Inject]
public Customer(IAddress address)
{
this.MainAddress = address;
}
public bool HasPoBox()
{
return MainAddress.HasPoBox();
}
public void DisplayMediaDeviceAddress()
{
MediaDevice.PrintAddress();
}
}
#endregion
Eine einfache Klasse die die beiden Interfaces verwendet… Und! Zum ersten mal Ninject, wie man sieht ist Ninject wirklich sehr schmal, es wird lediglich über das Inject Attribut angegeben, dass im nachfolgenden Codeblock eine Injection vorgenommen werden soll. Mehr nicht.
Die vier Dependency-Injection Patterns von Ninject
Ninject unterstützt vier verschiedene Arten von Dependency Injection, mit dessen Hilfe man lose Kopplungen in .NET Applikationen realisieren kann.
Constructor Injection
Bei der Constructor Injection versucht Ninject, sofern im Mapping definiert, die vorhandenen Abhängigkeiten aufzulösen und entsprechend dem Mapping die konkreten Typen zur Laufzeit zu verwenden.
#region DI Patterns
public class DependencyInjectionPatterns
{
[Inject]
public DependencyInjectionPatterns(IMediaDevice mediaDevice, IAddress address)
{
// do something with media device
// do somethinh with address
}
}
#endregion
Property Injection
Der klassische Weg, hierbei werden die Dependencies als Properties der Klasse angelegt und Ninject speist zur Laufzeit den korrekten Typ ein.
#region DI Patterns
public class DependencyInjectionPatterns
{
[Inject]
public IMediaDevice MyMediaDevice { get; set; }
}
#endregion
Field Injection
Die Field Injection funktioniert analog zur Property Injection.Auf die chronologische Reihenfolge hat man hierbei keinen Einfluss.
#region DI Patterns
public class DependencyInjectionPatterns
{
[Inject]
private IAddress businessAddress;
}
#endregion
Method Injection
Ninject speist hierbei die konkreten Typen für die Parameter der Methode ein. Auf die chronologische Reihenfolge hat man hierbei keinen Einfluss.
#region DI Patterns
public class DependencyInjectionPatterns
{
[Inject]
public String PrintAllInformation(IMediaDevice mediaService,
IAddress businessAddress,
IAddress homeAddress)
{
return String.Format("{0}-{1}-{2}",
new object[] {
mediaService.ToString(),
businessAddress.ToString(),
homeAddress.ToString()
});
}
}
#endregion
Soweit wäre das Beispiel fertig, lediglich das Mapping fehlt noch. Um Mappings mit Ninject zu realisieren werden sogenannte “Module” erstellt die später vom Ninject Kernel verarbeitet werden.
Ninject Module
Module sind bei Ninject wie bereits gesagt dafür verantwortlich, dass das Framework weiß welche Typen einzuspeisen sind. Im nachfolgenden Listing sind zwei Mapping Module angegeben, um später etwas mit dem “Injecting” spielen zu können
#region Ninject Modules
public class ProductiveModel : StandardModule
{
public override void Load()
{
Bind<IMediaDevice>().To<WindowsPhone>();
Bind<IAddress>().To<NationalAddress>();
}
}
public class StagingModule : StandardModule
{
public override void Load()
{
Bind<IMediaDevice>().To<IPhone>();
Bind<IAddress>().To<InternationalAddress>();
}
}
#endregion
Ninject Kernel
Der Ninject Kernel ist sozusagen die Schaltzentrale von Ninject, denn dem Kernel wird das zu ladende Module übergeben, so dass der Kernel weiss welche Typen er zur Laufzeit verwenden soll.
Im nachfolgenden Beispiel habe ich einen Wrapper um den Kernel gebaut um parallel beide Module im Speicher zu halten. Nach dem ersten Zugriff werden die Kernels immer aus dem Speicher zurückgegeben
#region Ninject Kernel Loading
public class ExampleNinjectKernel
{
public static readonly ExampleNinjectKernel Instance =
new ExampleNinjectKernel();
private IKernel stagingKernel;
private IKernel productiveKernel;
private ExampleNinjectKernel()
{
}
public IKernel GetKernel(bool staging)
{
if (staging)
{
if (stagingKernel != null)
return stagingKernel;
stagingKernel = new StandardKernel(new StagingModule());
return stagingKernel;
}
else
{
if (productiveKernel != null)
return productiveKernel;
productiveKernel = new StandardKernel(new ProductiveModel());
return productiveKernel;
}
}
}
#endregion
Einbindung in eine .NET Applikation
Mit den bis jetzt gezeigten Bestandteilen lässt sich nun bereits eine lauffähige, lose gekoppelte Anwendung erstellen. Ich habe der Einfachheit wegen eine Consolenanwendung ausgewählt. In der Program Klasse erstelle ich nun einfach die gewünschte Kernel Instanz und kann dann über die angebotenen Methoden fertig “injectete” Objektinstanzen anfragen.
Im nachfolgenden Code ist noch viele Calls auf die Console Klasse enthalten, um auch ein schönes Ergebnis zu bekommen :) lässt man diese weg, ist das ganze Szenario nicht größer als 10-15 Zeilen und somit gut lesbar.
static void Main(string[] args)
{
Console.Title = "Ninject DI Example...";
IKernel myKernel =
ExampleNinjectKernel.Instance.GetKernel(true);
Customer exampleCustomer =
myKernel.Get<Customer>();
#region some console actions
Console.ForegroundColor = ConsoleColor.Green;
Console.WriteLine("Print some Customer information...");
Console.WriteLine("Address is now type of '{0}'",
exampleCustomer.MainAddress.GetType().FullName);
Console.WriteLine("Media Device is now type of '{0}'",
exampleCustomer.MediaDevice.GetType().FullName);
Console.WriteLine("");
Console.ResetColor();
#endregion
exampleCustomer.DisplayMediaDeviceAddress();
exampleCustomer.MainAddress.PoBox = "123";
exampleCustomer.MainAddress.PrintPoBox();
#region some console actions
Console.ForegroundColor = ConsoleColor.Green;
Console.WriteLine("");
Console.WriteLine("Staging customer printed");
Console.ResetColor();
Console.WriteLine("");
#endregion
myKernel =
ExampleNinjectKernel.Instance.GetKernel(false);
exampleCustomer =
myKernel.Get<Customer>();
#region some console actions
Console.ForegroundColor = ConsoleColor.Green;
Console.WriteLine("Print some Customer information...[Productive]");
Console.WriteLine("Address is now type of '{0}'",
exampleCustomer.MainAddress.GetType().FullName);
Console.WriteLine("Media Device is now type of '{0}'",
exampleCustomer.MediaDevice.GetType().FullName);
Console.WriteLine("");
Console.ResetColor();
#endregion
exampleCustomer.DisplayMediaDeviceAddress();
exampleCustomer.MainAddress.PoBox = "4711";
exampleCustomer.MainAddress.PrintPoBox();
#region some console actions
Console.ForegroundColor = ConsoleColor.Green;
Console.WriteLine();
Console.WriteLine("productive customer printed");
Console.WriteLine("");
Console.WriteLine("");
Console.ResetColor();
Console.WriteLine("Press any key to exit...");
Console.ReadLine();
#endregion
}
Die Ausgabe der Anwendung sollte, wenn ihr alles korrekt implementiert habt so aussehen
In den kommenden Tagen werde ich noch weitere Posts zum Thema Ninject veröffentlichen. Ein wichtiges und interessantes Thema in Bezug auf Ninject sind Contextual Mappings. Durch den Einsatz von kontextabhängigen Bindings kann sich Ninject als DI-Framework erst richtig entfalten.
Fazit
Ich hoffe ich konnte mit diesem Post einigen Leuten eine nette Alternative zu Spring.NET nahebringen. Ich persönlich finde Ninject sehr schön, gerade weil es so schlank ist, eventuell ergibt es sich auch mal, dass ich Ninject in größeren Projekten einsetzen kann um das gesamte Portfolio einmal auszuschöpfen.