Dependency Injection und Lifecycle

Planung und Umsetzung von Abhängigkeiten sind das Thema diverser Prinzipien. Wer bei der Softwareentwicklung nicht auf die Struktur der Abhängigkeiten achtet, landet schnell im Chaos. Für viele Entwickler bedeutet das allerdings, dass sie „überall“ Interfaces einziehen. Als CCD Akademie betrachten wir das Thema etwas differenzierter und empfehlen den Einsatz des IOSP. Welche Auswirkung das auf Dependency Injection und das Lifecycle Management von Objekten hat, beschreibt dieser Artikel.

Prinzipien zum Thema Abhängigkeiten

Das Dependency Inversion Principle (DIP)

Im Zusammenhang mit Abhängigkeiten dürfte das Dependency Inversion Principle (DIP) am weitesten bekannt sein. Es besagt, dass eine direkte Abhängigkeit zwischen zwei Klassen durch das Einfügen eines Interface umgekehrt werden kann. Details können damit von Abstraktionen abhängig gemacht werden.

vom Dependency Inversion Principle zum Integration Operation Segregation Principle - Clean Code Developer Akademie - Trainings - Abb. 1
Abbildung 1
vom Dependency Inversion Principle zum Integration Operation Segregation Principle - Kontrakt zwischen A und B - Abb. 2
Abbildung 2

Die Änderung der Richtung ergibt sich aus der Tatsache, dass die implementierende Klasse (hier B) vom Interface oder Contract (hier Bc) abhängig ist. Die Abhängigkeitsrichtung ist umgekehrt zur ursprünglichen Richtung. Die Architekturmuster Hexagonale ArchitekturOnion Architecture und Clean Architecture machen sich dies zunutze: durch das DIP verlaufen hier die Abhängigkeiten von außen nach innen.

Das Integration Operation Segregation Principle (IOSP)

Als weiteres Prinzip befasst sich auch das Integration Operation Segregation Principle (IOSP) mit der Abhängigkeitsstruktur. Statt die Struktur als gegeben hinzunehmen und durch ein Interface die Schmerzen zu lindern, wird hier eine gänzlich andere Struktur empfohlen. Der Aspekt der Integration wird als eigenständiger Aspekt herausgelöst. Es entstehen so Methoden, deren einzige Aufgabe es ist, andere Methoden zu integrieren, indem sie diese aufrufen. Solche Integrationsmethoden haben also maximale Abhängigkeit und sind für nichts anderes verantwortlich. Aus diesem Grund kann hier vollständig auf Interfaces verzichtet werden. Details dazu siehe im Artikel DIP oder IOSP.

Integration Operation Segregation Principle (IOSP)
Abbildung 3

In vielen Fällen könnte sogar auf Dependency Injection verzichtet werden, da im Test nur sehr selten mit Attrappen gearbeitet werden muss. Doch es gibt neben der Testbarkeit einen anderen Grund für Dependency Injection: das Lifecycle Management von Objekten.

Dependency Injection / Abhängigkeitsinjektion

Dependency Injection ist zwar kein Prinzip sondern ein Muster. Trotzdem soll es hier genannt werden. Mit dem Begriff Dependency Injection wird der Vorgang bezeichnet, einer Klasse ihre Abhängigkeiten von außen bereitzustellen. Somit erstellt sich eine Klasse ihre Abhängigkeiten nicht selbst durch den Aufruf der jeweiligen Konstruktoren, sondern erwartet die benötigten Instanzen über eigene Konstrukturparameter. Benötigt bspw. die Klasse NewCustomer ein Repository, um damit ein neues Kundenobjekt in der Datenbank zu speichern, dann erwartet sie das Repository über einen Parameter im Konstruktor (hier mittels Interface).

				
					public class NewCustomer
{
    private readonly ICustomerRepository _customerRepository;

    public NewCustomer(ICustomerRepository customerRepository) {
        _customerRepository = customerRepository;
    }
}

				
			

Lifecycle Management

Transient

Objekte haben eine Lebenszeit. Diese beginnt mit der Instanziierung des Objekts und endet mit dem Dispose. Auf den managed Plattformen wie CLR und JVM findet das Entfernen der Objekte aus dem Speicher durch den Garbage Collector vollautomatisch statt, wenn keine Referenz mehr auf ein Objekt existiert. Solange ein Objekt noch über eine Referenz erreicht werden kann, „lebt“ es. Ist es nicht mehr über eine Referenz erreichbar, kann es entfernt werden. Der Lifecycle solcher Objekte wird als transient bezeichnet.

Singleton

Etwas schwieriger wird die Sache, wenn man das Thema Zustand hinzunimmt. Eine Klasse kann Zustand halten, indem sie Felder enthält. Felder liegen auf dem Heap, werden also pro Instanz eines Objektes bereitgestellt. Daraus ergibt sich die Frage, ob wir innerhalb einer Anwendung über eine oder mehrere Instanzen eines Objektes verfügen möchten. Neben transient haben wir damit das allzeit beliebte Singleton als zweiten Lifecycle.

Somit haben wir es bis hier her mit zwei möglichen Lebenszeiten der Objekte zu tun: wir müssen uns zwischen einem Singleton oder einem transienten Objekt entscheiden. Ein Singleton bedeutet, es gibt innerhalb der Anwendung genau eine Instanz des Objekts. Dies wird bspw. verwendet, um globale Konfigurationsdaten, wie etwa den Connectionstring zur Datenbank, bereitzustellen, die für die gesamte Anwendung gelten. Bei transienten Objekten dagegen wird jeweils bei Bedarf eine Instanz erzeugt. Das ist beispielsweise dann sinnvoll, wenn für einen Use Case kurzfristig bestimmte Objekte benötigt werden. Solange die Objekte existieren, halten sie ggf. auch Zustand. Solange dieser Zustand nicht über längere Zeit erhalten bleiben muss, ist ein transientes Objekt meist die Lösung.

Scoped

Eine dritte Variante entsteht, wenn eine Anwendung von mehreren Benutzern gleichzeitig verwendet werden kann, wie es bei Webanwendungen der Fall ist. In dem Fall erhält jeder Anwender eine eigene Instanz eines Objektes und verfügt somit über jeweils eigenen Zustand. Die Laufzeit solcher Objekte soll häufig an die Interaktion eines Benutzers gebunden werden. Dieser sogenannte scoped Lifecycle bedeutet, dass ein Objekt pro Anwender bzw. pro Scope erzeugt wird. Ferner wird es wieder freigegeben, wenn die Interaktion des Anwenders beendet ist. In der Anwendung muss der Scope definiert werden. Im Falle einer Webanwendung könnte dies bspw. der Request sein.

Dependency Injection / Abhängigkeitsinjektion

Egal wie man seine Abhängigkeiten entwirft, ob mit Dependency Inversion (DIP) oder der Trennung von Integration und Operation (IOSP), irgendwie müssen die Objekte instanziert und zusammengesetzt werden. Um flexibel zu bleiben bietet sich hier die Dependency Injection an. Und damit die Auflösung der benötigten Objekte nicht „von Hand“ erfolgen muss, setzt man am besten einen Dependency Inversion Container (auch als Inversion of Control Container bezeichnet) ein. Die Bezeichnungen DI Container und IoC Container sind synonym.

DI Container nutzen

Voraussetzung für eine Automatisierung des Lifecycle Managements ist, dass die Instanzen injiziert werden können. Die häufigste Variante ist dabei die Dependency Injection über Konstruktorparameter. Dies hat den Vorteil, dass damit eine klare Notwendigkeit ausgedrückt wird. Alternativ können optionale Abhängigkeiten über Property Injection aufgelöst werden. Das bietet sich an bei Abhängigkeiten, die optional sind.

Heute sind DI Container ein Standardwerkzeug. Frameworks wie ASP.NET Core oder Spring (Java) bringen diese Infrastruktur mit und setzen auf dieses Muster. Der Einsatz eines DI Containers ist im Sinne der Wandelbarkeit sehr sinnvoll: ändert sich die Abhängigkeitsstruktur und damit die Signatur eines Konstruktors, sorgt der DI Container dafür, dass alles zur Laufzeit zusammengesetzt wird. Das reduziert den Refactoring Aufwand bei Änderungen an einem Konstruktor. Somit bietet der Einsatz eines DI Containers eine Vereinfachung im Sinne der Wandelbarkeit.

Zum anderen bietet der DI Container die Möglichkeit, die Lebenszeit von Objekten per Konfiguration vorzugeben. Es muss also niemand mehr eine Singleton Implementierung vornehmen. Stattdessen wird der DI Container angewiesen, die Objekte eines bestimmten Typs als Singleton auszuliefern. So sorgt der DI Container dafür, dass zur Laufzeit jeweils genau eine Instanz von diesem Typ existiert.

Der DI Container ermöglicht das Abrufen von Instanzen und sorgt dafür, dass dabei rekursiv alle benötigten Objekte bereitgestellt werden.

Codebeispiele

Die folgenden Tests stellen dar, wie sich die drei Lebenszeiten für Objekte unterscheiden.

				
					
using Microsoft.Extensions.DependencyInjection;
using NUnit.Framework;

namespace dipfordi;

public class DI_Container_Demos
{
    [TearDown]
    public void Teardown() {
        Foo.NumberOfInstances = 0;
    }
    
    [Test]
    public async Task Ohne_ctor_Abhängigkeiten() {
        await using var provider = new ServiceCollection()
            .AddTransient<Foo>()
            .BuildServiceProvider();
        
        var foo = provider.GetService<Foo>();
        Assert.That(foo, Is.Not.Null);
    }

    [Test]
    public async Task Ctor_mit_Abhängigkeiten() {
        await using var provider = new ServiceCollection()
            .AddTransient<Foo>()
            .AddTransient<Bar>()
            .BuildServiceProvider();
        
        var bar = provider.GetService<Bar>()!;
        Assert.That(bar, Is.Not.Null);
        Assert.That(bar.Foo, Is.Not.Null);
    }

    [Test]
    public async Task Mehrere_transiente_Instanzen() {
        await using var provider = new ServiceCollection()
            .AddTransient<Foo>()
            .BuildServiceProvider();
        
        var foo1 = provider.GetService<Foo>();
        var foo2 = provider.GetService<Foo>();
        var foo3 = provider.GetService<Foo>();
        Assert.That(Foo.NumberOfInstances, Is.EqualTo(3));
        Assert.That(foo1, Is.Not.SameAs(foo2));
        Assert.That(foo2, Is.Not.SameAs(foo3));
    }
    
    [Test]
    public async Task Eine_Singleton_Instanz() {
        await using var provider = new ServiceCollection()
            .AddSingleton<Foo>()
            .BuildServiceProvider();
        
        var foo1 = provider.GetService<Foo>();
        var foo2 = provider.GetService<Foo>();
        var foo3 = provider.GetService<Foo>();
        Assert.That(Foo.NumberOfInstances, Is.EqualTo(1));
        Assert.That(foo1, Is.SameAs(foo2));
        Assert.That(foo2, Is.SameAs(foo3));
    }
    
    [Test]
    public async Task Diverse_scoped_Instanzen() {
        await using var provider = new ServiceCollection()
            .AddScoped<Foo>()
            .BuildServiceProvider();

        using (var scope1 = provider.CreateScope()) {
            var foo1 = scope1.ServiceProvider.GetService<Foo>();
            var foo2 = scope1.ServiceProvider.GetService<Foo>();
        }

        using (var scope2 = provider.CreateScope()) {
            var foo3 = scope2.ServiceProvider.GetService<Foo>();
            var foo4 = scope2.ServiceProvider.GetService<Foo>();
        }
        var foo5 = provider.GetService<Foo>();
        Assert.That(Foo.NumberOfInstances, Is.EqualTo(3));
    }
}

public class Foo
{
    public static int NumberOfInstances;

    public Foo() {
        NumberOfInstances += 1;
    }
}

public class Bar(Foo foo)
{
    public readonly Foo Foo = foo;
}

				
			

Fazit

Das Thema Abhängigkeiten ist die Never Ending Story der Softwareentwicklung. Nach wie vor werden hierbei so viele Fehler begangen, dass die Codequalität darunter deutlich leidet. Mit Codequalität meine ich, dass die beiden Werte Wandelbarkeit und Korrektheit nur schwer erreicht werden. Die Struktur der Abhängigkeiten hat einen maßgeblichen Einfluss darauf, ob der Code leicht verständlich und leicht änderbar ist. Ferner ergibt sich aus der Struktur, ob es leicht ist, den Code mit automatisierten Tests zu testen. Die erste Aufgabe besteht also darin, eine gut Abhängigkeitsstruktur sicherzustellen.

Darüber hinaus können Dependency Injection und DI Container als Tools verwendet werden. Sie vereinfachen das Refactoring von Konstruktoren und ermöglichen es per Konfiguration zu entscheiden, auf welche Weise Instanzen eines Typs erzeugt werden sollen. Selbst unterschiedliche Implementierungen eines Interface können über den DI Container aufgelöst werden und reduzieren die Kopplung zur Compilezeit, weil die Auflösung erst zur Laufzeit stattfindet.

Unsere Seminare

course
Clean Code Developer Basics

Prinzipien und Tests – Das Seminar wendet sich an Softwareentwickler, die gerade beginnen, sich mit dem Thema Softwarequalität auseinanderzusetzen. Es werden die wichtigsten Prinzipien und Praktiken der Clean Code Developer Initiative vermittelt.

zum Seminar »
course
Clean Code Developer Advanced

Mit Flow Design von den Anforderungen zum Clean Code – Lernen Sie mit Flow Design einen Softwareentwicklungsprozess kennen, der Sie flüssig von den Anforderungen zum Clean Code führt.

zum Seminar »
course
Clean Code Developer Trainer

Seminare als Trainer durchführen – Dieses Seminar wendet sich an Softwareentwickler, die ihr Wissen über die Clean Code Developer Prinzipien und Praktiken bzw. über Flow Design als Trainer an andere weitergeben möchten.

zum Seminar »

Kommentar verfassen

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert

de_DEGerman