Abhängigkeiten selbst gestalten: Mit Dependency Injection zu flexiblem Software-Design

Autor / Redakteur: Stephan Roth * / Sebastian Gerstl

Um ein funktionierendes und modulares Softwaresystem zu erhalten, sind Abhängigkeiten im Softwaredesign leider bis zu einem gewissen Grad unvermeidlich. Daher sollten sie vom Entwickler von vorneherein gezielt angelegt werden, so dass sie möglichst wenig stören. Das Entwurfsmuster Dependency Injection (DI) hilft hierbei.

Anbieter zum Thema

Es gibt zahlreiche Möglichkeiten Abhängigkeiten so zu gestalten, dass sie wenig bis gar keine Schwierigkeiten bereiten. Mit der so genannten Dependency Injection (DI) lassen sich Abhängigkeiten aus dem Entwurf herausziehen und gezielt zur Laufzeit auflösen.
Es gibt zahlreiche Möglichkeiten Abhängigkeiten so zu gestalten, dass sie wenig bis gar keine Schwierigkeiten bereiten. Mit der so genannten Dependency Injection (DI) lassen sich Abhängigkeiten aus dem Entwurf herausziehen und gezielt zur Laufzeit auflösen.
(Bild: Clipdealer)

Abhängigkeiten (engl. dependencies) im Softwaredesign sind prinzipbedingt eigentlich immer schlecht, aber um ein funktionierendes und modulares Softwaresystem zu erhalten sind sie bis zu einem gewissen Grad unvermeidlich. Daher ist es vielmehr die Aufgabe der Softwareentwickler, und vor allem auch die Verantwortung der Softwarearchitekten, Abhängigkeiten derart zu gestalten, dass sie möglichst wenig „Schmerzen“ verursachen. Mit dem Entwurfsmuster Dependency Injection (DI) können Abhängigkeiten sogar externalisiert, also: aus dem Entwurf herausgezogen, und erst zur Laufzeit aufgelöst werden.

Abhängigkeiten im Softwaredesign – Die Wurzel allen Übels

Mit Hilfe des bekannten Ansatzes Teile und Herrsche (lat.: Divide et impera) werden komplexe Softwaresysteme nach bestimmten Kriterien und Entwurfsprinzipien hierarchisch dekomponiert, um die dadurch entstehenden kleineren Teile besser verstehen, leichter handhaben, und auch einfach testen zu können. Bei dieser Vorgehensweise entstehen Softwarebausteine auf verschiedenen Abstraktionsebenen: Angefangen bei den relativ großen Komponenten (auch: Subsystemen) bis hinunter zu kleinen Modulen, wie beispielsweise die bekannten Klassen aus der Objektorientierung. Leitende Entwurfsprinzipien für das Finden dieser Komponenten sind u.a. das Single Responsibility Principle (SRP), Separation of Concerns (SoC) oder das Geheimnisprinzip (Information Hiding) [1].

Bildergalerie
Bildergalerie mit 5 Bildern

Doch nicht selten ist auch nach einer solchen hierarchischen Zerlegung des Systems die Evolvierbarkeit (siehe Definition im Kasten unten) der Software überraschend schlecht. Selbst wenn gemäß dem SRP die Zuständigkeiten der einzelnen Komponenten und Module klar definiert worden sind, widersetzt sich das System dennoch oftmals gegenüber Änderungen und neuen Anforderungen. Der Grund dafür sind nicht selten zu viele oder falsch gerichtete Abhängigkeiten zwischen den Bausteinen, die aus dem vermeintlich modularisierten System letztendlich doch wieder einen starren Monolithen entstehen lassen.

Unter einer Abhängigkeit versteht man beispielsweise im objektorientierten Softwaredesign die Tatsache, dass eine beliebige Klasse A zu ihrer Aufgabenerfüllung eine andere Klasse B benötigt (siehe Bild 1). Anders ausgedrückt: Die Klasse A ist ohne die Klasse B hinsichtlich ihrer Spezifikation oder Implementierung unvollständig.

Die in dieser Darstellung verwendete Beziehung nennt sich in der Unified Modeling Language (UML) dependency relationship [2} und ist noch sehr abstrakt, d.h. sie sagt nur aus, dass A irgendwie von B abhängig ist. In UML-Klassen- und Komponentendiagrammen gibt es deshalb noch weitere Beziehungen, die ebenfalls Abhängigkeiten darstellen, aber eine konkretere Semantik haben. Der folgende C++-Quellcode enthält beispielsweise diverse Abhängigkeiten unterschiedlicher Art, die in der darauf folgenden Bild 2 einmal grafisch visualisiert sind.

#include
#include
// ...more includes here...

class IF {
public:
    virtual void doSomething() = 0;
    virtual ~IF() = default;
};

class A : public IF {
public:
    virtual void doSomething() override { }

private:
    std::shared_ptr e;
};

class B : public A {
public:
    void doSomethingMore(const C& param) {
        S* pointerToS = S::getInstance();
        }

    D querySomething() const { return D(); }

private:
    std::vector f;
};

Die Abhängigkeit, die wohl am wenigsten Schwierigkeiten bereiten dürfte, ist die Realisierungsbeziehung (engl. interface realization relationship) zwischen der Klasse A und dem «interface» IF (Da es, im Gegensatz zu z.B. Java, keine Interfaces in der Sprache C++ gibt, werden diese durch Klassen mit ausschließlich rein virtuellen Methoden emuliert). Abstraktionen (Schnittstellen bzw. abstrakte Klassen) drücken erfahrungsgemäß Stabilität aus, daher ist es eher angenehm, wenn man sich von ihnen abhängig macht.

Bei der Klasse B sieht die Situation hingegen schon ganz anders aus. Wie man der Bild 2 leicht entnehmen kann, ist B gleich von 5 anderen Elementen unmittelbar abhängig. Die stärkste Abhängigkeit ist dabei die Generalisierungsbeziehung (engl.: generalization relationship) zwischen B und A, umgangssprachlich auch als Vererbung bezeichnet. Dieses ist eine besonders enge Kopplung zwischen B und A, da B nicht nur die Schnittstelle, sondern auch die Implementierung von A erbt (whitebox re-use).

Obwohl die zuvor genannte Vererbung bereits eine sehr starke Kopplung darstellt, dürfte die Verwendungsabhängigkeit (engl. usage dependency) zwischen der Klasse B und dem globalen Singleton S wohl die unerfreulichste aller Beziehungen darstellen. Diese Abhängigkeit ist nicht in der Klassenschnittstelle von B sichtbar bzw. zugänglich, sondern sie ist irgendwo in der Implementierung versteckt. Das bedeutet u.a. auch, dass z.B. die isolierte Testbarkeit von B, wie sie für einen Unit-Test erforderlich ist, erheblich erschwert wird – S lässt sich nicht so ohne weiteres durch eine Testattrappe (Mock-Objekt) ersetzen.

Zyklische Abhängigkeiten

Eine weitere, ganz besonders unangenehme und starke Kopplung entsteht, wenn sich zwei (oder mehr) Klassen wechselseitig referenzieren – die sogenannte zyklische (oder zirkuläre) Abhängigkeit, wie in Bild 3 dargestellt.

Bildergalerie
Bildergalerie mit 5 Bildern

Eine solcher Abhängigkeitszyklus kann sogar verheerende Auswirkungen auf die Softwarearchitektur haben, nämlich dann, wenn die daran beteiligten Elemente auf den entgegengesetzten Seiten einer Architektur-Grenze liegen, beispielsweise in verschiedenen Komponenten (siehe Bild 4), oder auf verschiedenen Schichten einer Schichtenarchitektur (layered architecture).

Wie leicht zu erkennen ist, werden durch die zyklische Abhängigkeit zwischen A und B auch die Komponenten X und Y untrennbar miteinander vereinigt, was erheblich nachteilige Auswirkungen auf die (Wieder-)Verwendbarkeit und Testbarkeit der Komponenten hat, und somit auch die Strukturierung der Software in Komponenten letztendlich ad absurdum führt.

(ID:45468332)