Work in Progress
Diese Seite ist aktuell im Review! Die Seite wurde noch nicht qualitätsgesichert und kann Fehler enthalten.
Die verlinkten Seiten sind ggf. nur für Schleupen-Mitarbeiter sichtbar.

Dependency Injection - Application-Integration

In unterschiedlichen Szenarien sind oft unterschiedliche Implementierungen einer Schnittstelle erforderlich - gerade hierfür werden ja Schnittstellen verwendet: Zum Test einer Klasse, die beispielsweise einen Service verwendet, ist es wünschenswert, ein Testdouble zu verwenden. In einem anderen Ausführungskontext dagegen ist der Einsatz der echten Implementierung erforderlich.

Um nicht in diesen unterschiedlichen Ausführungskontexten manuell die Abhängigkeit zu injizieren oder unterschiedliche Anbindungen manuell zu implementieren, soll ein flexibler Mechanismus verwendet werden, der auch konfigurierbar ist. Dieses Muster beschreibt die Einbindung der Dependency Injection im einzelne Anwendungen (z.B. .exe, Windows-Dienste).

Design

Das Programm, in dem die Dependecy Injection angeschlossen werden soll, verwendet den DependencyInectionContainer. Außerdem ist ein Konfigurator zur Einrichtung dieses DependencyInectionContainers nötig.

Implementierung

In diesem Beispiel ist eine Konsolenanwendung zu sehen, die den DependencyInjectionContainer angeschlossen und konfiguriert hat und ihn dann verwendet. Im Konstruktor wurde hier der Konfigurator verwendet, um den Container einzurichten. Diese Einrichtung ist immer einmalig notwendig und ist an geeigneter Stelle zu erfolgen, z.B. in der Main()-Methode.

public sealed class Program
{
    private readonly ITestClass testClass;

    private Program(ITestClass testClass)
    {
        this.testClass = testClass;
    }

    public static void Main(string[] args)
    {
        using (var container = new DependencyInjectionContainer())
        {
            var configurator = new MyContainerConfigurator();
            configurator.Configure(container.Windsor);

            var testClass = container.Windsor.Resolve<ITestClass>();
            try
            {
                var program = new Program(testClass);
                program.DoSomething();
            }
            finally
            {
                container.Windsor.Release(testClass); // Instanzen müssen immer freigegeben werden!
            }
        }
    }

    private void DoSomething() => testClass.Foo();
}

In diesem Beispiel verwendet der Konfigurator die ausgeführte Exe als einzige Assembly, die von Castle verwendet werden soll.

Hier sollte man in der Regel keine Framework-, SB- oder anderen API-Assemblies angeben. Es gibt normalerweise Installer-Klassen für APIs, die sich u.a. um die richtigen Lifestyles von Komponenten kümmert.

Ab S19: Wenn eine API ein Assembly-Attribute mit Namen EnforceExplicitDependencyRegistrationAttribute hat, gibt es beim Konfigurieren des Castle-Containers einen Fehler, wenn diesen API in der Liste von Assemblies steht.

internal sealed class MyContainerConfigurator : ContainerConfigurator
{
    protected override IEnumerable<Assembly> Assemblies
    {
        get
        {
            yield return Assembly.GetEntryAssembly();
        }
    }
}

Durch Castle erzeugte Instanzen müssen nach ihrer Verwendung wieder freigebeben werden. Dies geschieht durch den Aufruf der Release()-Methode. Da Castle die Instanzen intern referenziert, würde es ansonsten zu einem Memory Leak kommen, da diese nicht durch den Garbage Collector gelöscht würden!

Konfiguration

In der Implementierung des Konfigurators kann die Standardkonfiguration abgeändert werden. Dazu stehen einige überschreibbare Methoden zur Verfügung.

internal sealed class MyConfigurator : ContainerConfigurator
{
    protected override IEnumerable<Assembly> Assemblies
    {
        get { yield return typeof(FrameworkConfigurator).Assembly; } // Angabe der durch Castle zu verwendenen Assemblys
    }

    protected override void SetupWiring(Assembly assembly, BasedOnDescriptor descriptor)
    {
        descriptor.WithServiceDefaultInterfaces(); // Auswahl des Mappings, hier der Standard
    }

    protected override void SetupLifestyle(Assembly assembly, BasedOnDescriptor descriptor)
    {
        descriptor.LifestyleTransient(); // Auswahl des Lifestyles, siehe dazu http://docs.castleproject.org/Default.aspx?Page=LifeStyles&NS=Windsor
    }

    protected override bool UseType(System.Type type)
    {
        return true; // Zusätzliche Filterung der Typen möglich, um z.B. einige zu ignorieren
    }
}

Diagnose

Bei möglichen Fehlern werden in die Traceausgabe hilfreiche Meldungen geschrieben. Dazu ist lediglich die Aktivierung des Traceswtiches Schleupen.Framework.DependencyInjection nötig. Eine genause Beschreibung befindet sich im Castle-Wiki.

Abgrenzung

Es soll hiermit kein Plugin-Mechanismus bereitgestellt werden!

Stärken und Schwächen

Stärken
  • Der DI-Container wird nicht direkt sichtbar im Produktivcode verwendet, was eine bessere Trennung von Technik und Fachlichkeit darstellt
  • Austauschbarkeit der DI-Container-Bibliothek einfacher
  • Keine Create()-Methoden und somit weniger Code
  • Keine Reset()-Methode für Unittests notwendig
  • Anhand der Anzahl der Übergabeargumente im Konstruktor kann erkannt werden, dass zu viele Abhängigkeiten vorliegen, was ein schlechtes Design impliziert
  • Es wird ein Schnittstellen-orientiertes Design gefördert
Schwächen
  • Das Zusammenstellen für Unittests, die z.T. integrativ sind, ist schwer
  • Impliziter Mechanismus, d.h. schwierige Analyse bei Problemen
Cookie Consent mit Real Cookie Banner