Drucken

Keine Angst vor der Spritze

Eine Injektion beim Arzt zu erhalten wird von vielen Menschen als sehr unangenehm empfunden; Personen, die unter einer Nadelphobie leiden, bekommen sogar Herzrasen, Schwindelgefühle und Panikattacken. Das Injizieren von Abhängigkeiten (engl.: Dependency Injection, DI) im Software-Design hingegen ist völlig schmerzfrei, löst die starke Kopplung und Verdrahtung von Abhängigkeiten, schafft überschau- und wartbaren Code und fördert die Wiederverwendbarkeit.

Das Problem: zu viele Abhängigkeiten

In einem grossen Software-Projekt sind eine Vielzahl von Abhängigkeiten zwischen den einzelnen Komponenten unvermeidbar. Das muss man einfach als systemimmanentes Faktum hinnehmen und ist auch grundsätzlich kein Problem. Unangenehm wird es dann, wenn zu viele und zum Teil auch unnötige Abhängigkeiten existieren, so dass die Wartbarkeit des Systems rapide sinkt. Plötzlich haben selbst kleine Änderungen unvorhersehbare Nebeneffekte, neue Fehler schleichen sich ein und das System wird fragil. Der Aufwand für Modifikationen ist nur noch schwer abschätzbar, weil man nicht genau weiß, wie viele Komponenten man "anfassen" und ändern muss. Die Management-Vorgabe lautet daher meistens: „Never touch a running system!“

Zur Veranschaulichung einer sehr starken Abhängigkeit und der damit verbundenen Problematik hier ein kleines Code-Beispiel:

// File: DatabaseThingie.hpp

class Data;

// Objects of this class represent a kind of database interface
class DatabaseThingie
{
public:
    DatabaseThingie(void);
    ~DatabaseThingie(void);

    void insert(Data* data); // Store data in database
    Data* select(void); // Load data from database
};
// File: Example.hpp

class DatabaseThingie;  // Forward declaration

class Example
{
public:
    Example(void);
    ~Example(void);

    void doSomething(void);

private:
    DatabaseThingie*	_myDatabase;
};
// File: Example.cpp

#include "Example.hpp"
#include "DatabaseThingie.hpp"

Example::Example(void) : _myDatabase(new DatabaseThingie())
{
}

Example::~Example(void)
{
    delete _myDatabase;
}

void Example::doSomething()
{
    Data* data;
    data = _myDatabase->select();
    // Do some stuff with the queried data here...
    _myDatabase->insert(data);
}

Der Code ist nicht falsch! Objekte der Klasse Example benutzen Objekte vom Typ DatabaseThingie, um Daten aus einer Datenbank zu laden (DatabaseThingie::select) und auch wieder zu persistieren (DatabaseThingie::insert). Dennoch ist dieses Design problembehaftet, denn sobald die Anforderung gestellt wird, das man die Daten auch in einem Filesystem laden/speichern, oder per TCP/IP-Verbindung übertragen können muss, ist die starke Abhängigkeit von Example zu DatabaseThingie sehr störend. Zudem wird das Testen erheblich erschwert, denn was will man tun, wenn man in einer Umgebung testet, in der man nicht über eine Datenbank verfügt und DatabaseThingie somit keine Verbindung aufbauen kann?

Eine Lösung: Dependency Injection

An dieser Stelle kommt nun Dependency Injection ins Spiel! Die Idee ist, das man die Klasse Example nicht unmittelbar abhängig von einer konkreten Implementierung einer Persistenzlösung macht, sondern das man diese Abhängigkeit von aussen in Example "injiziert". Dafür müssen wir zunächst abstrahieren und eine abstrakte Klasse (In Java™: ein Interface) einführen, welche als Basisklasse für alle konkreten Persistenzklassen dienen kann:

// File: Persistence.hpp

class Data;

// The abstract base class for all concrete persistence classes.
class Persistence
{
public:
    virtual void save(Data* data) = 0;
    virtual Data* load(void) = 0;
};

Nun wird Example so abgeändert, das Example eine Instanzvariable vom Typ Persistence besitzt, und das man bei der Erzeugung einer Instanz von Example eine Instanz von Persistence als Konstruktor-Parameter mitgibt:

// File: Example.hpp (modified)
class Persistence;

class Example
{
public:
    // Constructor injection:
    Example(Persistence& persistenceObj);
    ~Example(void);

    void doSomething(void);

private:
    Persistence& _persistenceObj;
};

Persistence dient als Basisklasse von z.B. DatabasePersistence:

// File: DatabasePersistence.hpp
#include "Persistence.h"

class DatabasePersistence : public Persistence
{
public:
    DatabasePersistence(void);
    virtual ~DatabasePersistence(void);

    virtual void save(Data* data);
    virtual Data* load(void);
};

Bei der Erzeugung von Example wird nun ein Objekt vom Typ DatabasePersistence als Parameter übergeben (sog. Constructor Injection):

// Create object for database persistence
DatabasePersistence persistence;
// Create Example-object and "inject" the persistence object
Example example(persistence);
example.doSomething(); // Uses DatabasePersistence

DatabasePersistence ist nur eine von vielen, denkbaren, konkreten Persistenz-Klassen. So ist beispielsweise eine Klasse FilePersistence vorstellbar, mit der man eine Instanz von Example in einem Dateisystem persistieren kann. Eine Klasse MockPersistence kann als Platzhalter (Dummy) für eine reale Persitenz-Implementierung dienen und somit die Testbarkeit erhöhen, usw. Dieses UML-Klassendiagramm illustriert einige Möglichkeiten.

Darüber hinaus wäre es auch noch möglich, das Example Setter besitzt um die Abhängigkeit zu injizieren (sog. Setter Injection). Der Vorteil von Setter Injection ist das die Art und Weise, wie Example Daten lädt und/oder persistiert, zur Laufzeit austauschbar wäre!

// Example.hpp:
void setPersistence(Persistence& persistence);

// Example.cpp:
void Example::setPersistence(Persistence& persistence)
{
    _persistence = persistence;
}

Die dritte Möglichkeit Abhängigkeiten zu injizieren ist Interface Injection. Dazu wird in C++ eine abstrakte Klasse (In Java™: ein Interface) definiert, um die Injection zu kennzeichnen. Die abhängigen Klassen implementieren die rein virtuellen Methoden dieser Basisklasse.

class InjectPersistence
{
    virtual void injectPersistence(Persistence* pPersistence) = 0;
};
class Example : public InjectPersistence
{
    //...
    virtual void injectPersistence(Persistence* pPersistence);
    //...
};

Wie man sieht, ist Dependency Injection eine effektive Methode, um das Design von Software flexibel zu machen, Abhängigkeiten zu minimieren und die isolierte Testbarkeit von Code zu erleichtern. Objekte lassen sich nun zur Laufzeit quasi "zusammenstecken" und können voneinander auch wieder gelöst und neu konfiguriert werden, was die Bildung von Varianten von ein- und derselben Applikation ermöglicht.

„Dependency Injection is a key element of agile architecture“ (Ward Cunningham)

DI ist eines der zentralen Konzepte in sog. Inversion of Control (IoC) Frameworks, wie z.B. das im Java™-Umfeld sehr populäre Spring-Framework oder PocoCapsule für C++. Doch dazu ein anderes Mal mehr...

Amazon Ads...

Hosted by...

Publicons

Google Ads...

Creative Commons License

Dieses Werk bzw. dieser Inhalt steht unter einer Creative Commons Namensnennung-Nicht-kommerziell-Weitergabe unter gleichen Bedingungen 3.0 Deutschland Lizenz.

Fork me on GitHub