Blog Dependency Injection - Theorie und Praxis am Beispiel von Google Guice

Dependency Injection - Theorie und Praxis am Beispiel von Google Guice

n-design ist OSGi -Verfechter der ersten Stunde. Dabei basiert die √úberzeugung von dieser Technologie allerdings nicht auf der Technologie als solcher, sondern vielmehr auf OSGis Grundprinzipien, die zu einer strukturierten Architektur eines Softwaresystems durch Modularisierung und Serviceorientierung f√ľhren. In einer solchen Architektur bildet Dependency Injection einen grundlegenden Baustein. Gilt es nun Technologien f√ľr die Umsetzung neuer Projekte auszuw√§hlen, steht OSGi als bevorzugtes und beherrschtes Mittel im Vordergrund.

Was jedoch, wenn nun kein komplexes System, sondern lediglich ein einfaches Java-Programm, ein (kommandozeilenbasiertes) Tool oder ein strukturiertes Maven-Plugin umgesetzt werden soll? Hier scheint OSGi als Technologie sicherlich √ľberdimensioniert. Als leichtgewichtige Alternative konnten hier zuletzt gute Erfahrungen mit Google Guice gesammelt werden.

In diesem Blog-Beitrag soll das Thema Dependency Injection und die Design-Prinzipien, aus denen das Dependency Injection Pattern folgt, erläutert werden. Anschließend wird Google Guice als einfaches Framework zur Umsetzung von Modularität und Dependency Injection vorgestellt.

Dependency Injection

Stellen wir uns folgendes Klassendesign vor: Zur Umsetzung seiner Funktionalit√§t ist Klasse K vom konkreten Services S abh√§ngig. Um diesen verwenden zu k√∂nnen, wird in K eine Instanz dieser Serviceklasse erstellt. So ausreichend diese L√∂sung zur reinen Realisierung der Funktionalit√§t ist, f√ľhrt sie jedoch zu einigen Unannehmlichkeiten:

  • Ersetzen/Aktualisieren von S in K nicht ohne √Ąnderung des Quellcodes von K m√∂glich
  • S muss in seiner konkreten Implementierung als Abh√§ngigkeit von K bereits zur Kompilierzeit bekannt sein
  • K ist schwer testbar, da es nicht m√∂glich ist, S zu Testzwecken mit definierbarem Verhalten zu versehen ( Mocking )

Eine Möglichkeit diese Unannhemlichkeiten zu beheben, stellte Martin Fowler in dem Artikel Inversion of Control Containers and the Dependency Injection pattern vor. Dependency Injection stellt dabei ein Designprinzip dar, dem die Idee eines Plugin-Mechanismus zugrunde liegt. Anstatt dass K den abhängigen Service S selbst instanziiert um diesen verwenden zu können, wird eine entsprechende Instanz von S von einem Injector in K hereingereicht.

Design von Quellcode

Das Dependency-Injection-Designprinzip beeinflusst erheblich die Art und Weise, wie Klassenstrukturen entworfen werden. Jede Klasse hat aus fachlicher Sicht genau eine Aufgabe ( Single Responsibility ). Ben√∂tigt eine Klasse dar√ľberhinaus zu Umsetzung der eigenen fachlichen Aufgabe externe Funktionalit√§t, wird diese √ľber Abh√§ngigkeiten injiziert. Dies f√ľhrt aufgrund kleiner und fachlich spezifischer Klassen zu testbarem, wartbarem und austauschbarem Code, bei dem‚Ķ

  • ‚Ķ Klassen entkoppelt von Abh√§ngigkeiten sind, sodass Ersetzen/Aktualisieren ohne Quellcode-√Ąnderung geschehen kann.
  • ‚Ķ konkrete Implementierungen von Abh√§ngigkeiten nicht zur Kompilierzeit bekannt sein m√ľssen.
  • ‚Ķ durch Mocking von Abh√§ngigkeiten Klassen isoliert testbar sind.

Als großer Vorteil ergibt sich zusätzlich aus diesem Designprinzip, dass eine Abhängigkeit in seiner Funktionalität nicht mehr nur eine reine Library darstellen muss, die eine Menge von unabhängig voneinander aufrufbaren Funktionen kapselt. Vielmehr ergibt sich dadurch die Möglichkeit Frameworks zu gestalten. Ein Framework gibt im Gegensatz zu einer Library ein abstraktes Design und eine Verhaltenssteuerung vor, wobei konkretes Verhalten an definierten Stellen ins Framwork integriert werden kann.

Im vorliegenden Beispiel k√∂nnte K zum Beispiel ein mathematisches Verfahren implementieren, f√ľr dessen Umsetzung mehrere Teilschritte notwendig sind. Diese werden im Interface S abstrakt definiert. Deren konkrete Implementierungen k√∂nnen nun in SImpl umgesetzt und zur Laufzeit von einem Injector in K injiziert werden. Dabei bleiben die konkreten Implementierungen der einzelnen Teilschritte austauschbar.

Setter Injection und Constructor Injection

Setter Injection und Constructor Injection stellen zwei m√∂gliche Arten dar, wie Dependency Injection in Java konkret umgesetzt werden kann. Anhand der nachfolgenden Beispiel-Methode werden die beiden Arten gegen√ľber gestellt und miteinander verglichen.

public class SimpleLogger {
    private final Formatter formatter;

    public SimpleLogger() {
        this.formatter = new Formatter();
    }

    ...
}

Das obige Beispiel zeigt die Klasse SimpleLogger , die von einer Implementierung eines Formatter abhängig ist. Hier wird eine Instanz dieser abhängigen Klasse selbst erzeugt.

Setter Injection

Setter Injection beschreibt die Art der Injection, bei der Abh√§ngigkeiten √ľber hierf√ľr vorgesehene Setter-Methoden injiziert werden k√∂nnen, nachdem die Klasse bereits instanziiert wurde.

public class SimpleLogger {
    private Formatter formatter;

    public void setFormatter(Formatter formatter) {
        this.formatter = formatter;
    }

    ...
}

Vorteile:

  • Optionale Abh√§ngigkeiten:
    Die Umsetzung von optionalen Abh√§ngigkeiten wird dadurch m√∂glich, dass eine Instanz der Klasse erzeugt werden kann, ohne dass Abh√§ngigkeiten aufgel√∂st werden m√ľssen. Durch diese Freiheit k√∂nnen Abh√§ngigkeiten auch unaufgel√∂st bleiben.
  • Listen von Abh√§ngigkeiten:
    Durch mehrmaliges Aufrufen einer Setter-Methode können, bei entsprechender Implementierung, mehrere Instanzen eines gleichen Typs injiziert werden. So könnten zum Beispiel mehrere Implementierungen des Formatter injiziert werden, die hintereinander aufgerufen werden könnten.
  • Keine Vermischung:
    Technische Abh√§ngigkeiten werden √ľber Setter injiziert, w√§hrend Konstruktoren ausschlie√ülich f√ľr fachliche Abh√§ngigkeiten in Form von Daten, Konfigurationen etc. Verwendung finden. Hierdurch wird eine Vermischung von technischen und fachlichen Abh√§ngigkeiten verhindert und die Lesbarkeit des Sourcecodes erh√∂ht. Am Beispiel des SimpleLogger k√∂nnte dies bedeuten, dass ein Formatter √ľber einen Setter gesetzt wird, w√§hrend Informationen wie der Speicherort einer Log-Datei oder ein Log-Level √ľber den Konstruktor definiert werden.
  • √Ąnderung zur Laufzeit:
    Konkrete Instanzen von Abhängigkeiten können zur Laufzeit ausgetauscht werden, da Setter, anders als Konstruktoren, mehrfach aufgerufen werden können.

Nachteile:

  • Keine Forcierung:
    Durch die Tatsache, dass Instanzen von Klassen erzeugt werden k√∂nnen, ohne dass deren Abh√§ngigkeiten √ľber einen Konstruktor gesetzt werden m√ľssen , werden alle Abh√§ngigkeiten automatisch zu optionalen Abh√§ngigkeiten. Es kann nicht gew√§hrleistet werden, dass Abh√§ngigkeiten einer Instanz einer Klasse wirklich aufgel√∂st wurden.

Constructor Injection

Constructor Injection beschreibt die Art der Injection, bei der Abh√§ngkeiten √ľber einen hierf√ľr vorgesehenen Konstruktor bereits bei der Instanziierung der Klasse injiziert werden.

public class SimpleLogger {
    private final Formatter formatter;

    public SimpleLogger(final Formatter formatter) {
        this.formatter = formatter;
    }

    ...
}

Vorteile:

  • Forcierung:
    Ist eine Instanz einer Klasse erstellt, dann ist sichergestellt, dass deren Abhängigkeiten definitiv aufgelöst werden konnten. Eine Instanziierung einer solchen Klasse ist sonst nicht möglich.
  • Reihenfolge:
    Klassen und deren Abhängigkeiten werden in einer Baumstruktur modelliert und die Instanziierung ausgehend von den untersten Blättern vorgenommen. Hierdurch geschieht die Erzeugung in einer definierten Reihenfolge. Setter hingegen könnten zu jedem beliebigen Zeitpunkt aufgerufen werden, was demzufolge eine beliebige Reihenfolge der Instanziierung von Abhängigkeiten erlaubt.

Nachteile:

  • Keine optionalen Abh√§ngigkeiten:
    Optionale Abhängigkeiten sind im Konzept der Constructor Injection nicht vorgesehen. Dies könnte lediglich durch Überladung von Konstruktoren, wobei jede Kombination von Optionen umgesetzt sein muss, oder zusätzliche Setter erreicht werden.
  • Vermischung:
    Technische und fachliche Abh√§ngigkeiten werden in einem Konstruktor zusammengefasst. Dies f√ľhrt zu schlecht les- und wartbarem Code.
  • Statisch
    Es ist keine Ersetzung von abh√§ngigen Instanzen zur Laufzeit m√∂glich. Hier m√ľsste wiederum mit einem zus√§tzlichen Setter abhilfe geschaffen werden.

Google Guice

Google Guice ist ein Framework, mit welchem sich Dependency Injection in Java auf einfache Art und Weise umsetzen l√§sst. Es wurde von Google entwickelt, 2008 unter der Apache Lizenz ver√∂ffentlicht und befindet sich immer noch in reger Weiterentwicklung. Guice soll im folgenden vorgestellt werden, wobei jedoch ausschlie√ülich auf die Funktionalit√§t und nicht auf die interne technische Umsetzung eingegangen wird. Hierf√ľr sei auf die Internals -Section unter Google Guice verwiesen.

F√ľr diesen Blog-Beitrag wurde eine Beispiel-Applikation implementiert, die auf Github zu finden ist. Diese Applikation stellt eine vollst√§ndige Google Guice Applikation dar, der die nachfolgenden Beispiele entnommen wurden.

Verwendung

Google Guice findet zum Beispiel √ľber eine Maven-Dependency Einzug in ein Java-Projekt.

<dependency>
    <groupId>com.google.inject</groupId>
    <artifactId>guice</artifactId>
    <version>4.1.0</version>
</dependency>

Ist diese Dependency bezogen, bietet Guice eine Vielzahl von Annotationen , √ľber die Klassenstrukturen mittels Injections definiert werden k√∂nnen. Diese werden anschlie√üend in einem Modul gebunden und k√∂nnen √ľber einen Injector erstellt werden.

Injections

@Inject ist das neue new

Die Konfiguration von Injections basiert in Guice vornehmlich auf Annotationen . Dabei ist die wichtigste Annotation wohl @Inject . @Inject veranlasst das Framework, im hiermit annotierten Element eine abhängige Klasse zu ermitteln und die Injektion einer Instanz dieser Klasse vorzunehmen.

Dabei unterst√ľtzt Guice, um obige Theorie der Dependency Injection aufzugreifen, sowohl Setter- als auch Constructor-Injection , und dar√ľber hinaus sogar eine √ľber Reflection realisierte Field-Injection . Diese werden nachfolgend anhand des abgebildeten Interfaces Logger und einer entsprechenden Implementierung in Form eines SimpleLogger mit Abh√§ngigkeit zu einem Formatter in konkreten Beispielen vorgef√ľhrt.

public interface Logger {
    ...
}

Constructor Injection

public class SimpleLogger implements Logger {
    private final Formatter formatter;

    @Inject
    public SimpleLogger(final Formatter formatter) {
        this.formatter = formatter;
    }

    ...
}

Setter Injection

public class SimpleLogger implements Logger {
    private Formatter formatter;

    @Inject
    public void setFormatter(Formatter formatter) {
        this.formatter = formatter;
    }

    ...
}

Field Injection

public class SimpleLogger implements Logger {
    @Inject
    private Formatter formatter;

    ...
}

Bindings

Bei Verwendung einer @Inject -Annotation sucht das Framework in einer Menge von Bindings nach der Instanz einer konkreten Klasse, die an den Typ des annotierten Elements gebunden ist. Bindings beschreiben also, welche konkreten Instanzen von injizierbaren Klassen an welche Typen gebunden sind. Dabei unterschiedet Guice zwischen einer Vielzahl von möglichen Bindings, die im folgenden vorgestellt werden.

Linked Bindings

Linked Bindings beschreiben Bindings, bei denen konkrete Klassen an abstrakte Typen gebunden sind. Die gewöhnlichste Forms dieses Bindings stellt dabei eine Implementierung eines Interfaces dar, die an den Typen des Interfaces gebunden wird. So wird etwa die konkrete Klasse SimpleLogger an das Interface Logger gebunden. Möglich ist aber auch ein Binding, bei dem eine konkrete Klasse an den Typ einer Unterklasse gebunden ist.

bind(Logger.class).to(SimpleLogger.class);

Untargeted Bindings

Untargeted Bindings sind Bindings, bei denen konkrete Typen nicht an abstrakte Typen gebunden werden. Hierbei ist ein mit @Inject beschriebenes Element mit dem konkreten Typ einer Klasse versehen.

bind(SimpleLogger.class);

Named Annotations

√úber die @Named -Annotation k√∂nnen mehrere konkrete Implementierungen eines Typs gebunden und √ľber Namen explizit referenziert werden. Als Beispiel k√∂nnte man sich hier vorstellen, dass mehrere Implementierungen eines Formatter in verschiedenen Auspr√§gungen existieren, die mit unterschiedlichen Namings gebunden werden.

Das Binding geschieht hierbei √ľber eine zus√§tzliche Methode annotatedWith :

bind(Formatter.class)
    .annotatedWith(Names.named("Default"))
    .to(DefaultFormatter.class);

Die Referenzierung beim Inject geschieht √ľber die @Named -Annotation:

public class SimpleLogger implements Logger {
    private Formatter formatter;

    @Inject
    public void setFormatter(@Named("Default") Formatter formatter) {
        this.formatter = formatter;
    }

    ...
}

Instance Bindings

Ein Instance Binding erlaubt das Binding einer konkreten Instanz einer Klasse an einen abstrakten Typ. Am Beispiel des SimpleLogger könnten eine Instanz dieses SimpleLoggers händisch erzeugt und an den Typ Logger gebunden werden.

Logger logger = new SimpleLogger();
bind(Logger.class)
    .toInstance(logger);

Dar√ľberhinaus k√∂nnte ein solches Instance Binding dazu verwendet werden, um Value-Objects, zum Beispiel globale Konfigurationsparameter, zu binden.

bind(String.class)
    .annotatedWith(Names.named("LogFile"))
    .toInstance("/home/moe/logs/logger.log");

AbstractModule

Zur Umsetzung von Modulen liefert Guice eine abstrakte Klasse AbstractModule , die von einem konkreten Modul, zum Beispiel im vorliegenden Fall etwa durch die konkrete Klasse LoggingModule , erweitert wird. AbstractModule definiert dabei eine abstrakte Methode configure() , die gewissermaßen die Komposition von Interfaces, deren Implementierungen und einer Menge von konkreten Instanzen mittels Bindings zu einem Modul realisiert.

public class LoggingModule extends AbstractModule {
    @Override
    protected void configure() {
        bind(Logger.class).to(SimpleLogger.class);
        ...
    }

    ...
}

Injector

Die Komposition von Modulen zu einem System werden in Google Guice √ľber einen Injector realisiert. Der Injector wird dabei mit einer Menge von konkreten AbstractModule -Instanzen initialisiert, stellt zun√§chst die Relationen zwischen allen Objekten fest und erstellt hieraus einen Klassenhierarchien beschreibenden Objektgraphen . Anhand dieses Objektgraphen werden bereits bei der Erzeugung des Injectors alle notwendigen Instanzen von Klassen erstellt, und deren Abh√§ngigkeiten mittels Dependency Injection aufgel√∂st.

public class Main {
    public static void main(String[] args) {
        Injector injector = Guice.createInjector(
            new LoggingModule(),
            new FormatterModule(),
            new ConfigurationModule()
        );
        ...
    }
}

Einzelne Instanzen können anschließend beim Injector abgefragt werden:

Logger logger = injector.getInstance(Logger.class);

But wait, there’s more!

Neben den hier vorgestellten Elementen bietet Google Guice einige weitere, die im Guice-Wiki nachzulesen sind und hier nur stichpunktartig ausgef√ľhrt werden.

  • Bindings:
    • Constructor Binding : Erlaubt das Binding eines Typs an einen spezifischen Konstruktor einer Klasse.
    • Provides -Methode: Call-Back Methode, die zur Instanziierung eines bestimmten Typs verwendet wird, wobei das Binding √ľber die Annotation @Provides hergestellt wird.
    • Provider -Bindings: Provided Methods nicht √ľber Annotation @Provides , sondern durch die Implementierung eines Provider -Interfaces.
    • Built-in Bindings: Von Guice bereitgestellte Bindings (z.B. java.util.logger.Logging ).
    • Just-In-Time Bindings
  • Scopes :
    • Standardm√§√üig wird bei jeder Injection eine neue Instanz der zu injizierenden Klasse erzeugt. Scopes bieten hier die M√∂glichkeit der Wiederverwendung von Instanzen. Hierzu existieren nachfolgende Klassen-Annotationen.
    • @Singleton : Wiederverwendung √ľber den gesamten Lebenszyklus der Applikation
    • @SessionScoped : Wiederverwendung √ľber eine gesamte Session
    • @RequestScoped : Wiederverwendung √ľber einen Request
  • Extensions :
    • Guice bietet ein Service Provider Interface zur Implementierung von Erweiterungen und Plugins.
    • Es existieren einige offizielle Erweiterungen/Plugins.
    • Besonders Interessant: Multibindings - Binding eines Interfaces zu einer Liste von Implementierungen

Zusammenfassung

Unix-Philosophie: Make each program do one thing well.

Google Guice bietet Dependency Injection auf einfache Art und Weise und ist dabei deutlich leichtgewichtiger als etwa OSGi oder Spring . Der Grund liegt hier in der Fokussierung auf die eigentliche Kernfunktionalit√§t: W√§hrend OSGi und Spring deutlich mehr Funktionali√§t √ľber Dependency Injection hinaus bieten, unterst√ľtzt Guice ‚Äúlediglich‚ÄĚ Dependency Injection und eine einfache aber sehr effektive M√∂glichkeit der Modularisierung und Serviceorientierung.

Dabei fundiert diese Einfachheit auf einer ebenso einfachen Architektur:

  • Injections beschreiben Injektionen auf Klassenebene
  • Bindings definieren injizierbare Typen und deren konkrete Auspr√§gung und bilden somit den Grundstein f√ľr Services
  • AbstractModules komponieren √ľber Bindings eine Menge von Klassen zu einem Modul
  • √úber einen Injector wird ein System aus einer Menge von Modulen komponiert, das eine Klassenhierarchie identifiziert, Klassen instanziiert und deren Abh√§ngigkeiten injiziert.

Zus√§tzlich scheint sich Google Guice als hervorragende Grundlage zur Entwicklung eines ‚Äúeigenen‚ÄĚ Dependency-Injection-Frameworks zu eignen. Weiterentwicklungen mit Google Guice im Kern k√∂nnten sich etwa in folgenden Auspr√§gungen gestalten:

1. Properties-basierte Bindings zur Laufzeit

Bindings werden nicht mehr statisch im Sourcecode vorgenommen, sondern werden in Properties-Dateien beschrieben:

# Linked Binding
de.ndesign.blog...api.LoggingService = de.ndesign.blog...core.SimpleLoggingService
# Untargeted Binding
de.ndesign.blog...core.MessageFragmentUtility =
# Named Annotation + Instance Binding
@Named#LogPath = /home/moe/log/logger.log

Hieraus w√ľrden zur Laufzeit folgende Bindings vorgenommen:

bind(LoggingService.class).to(SimpleLoggingService.class);
bind(MessageFragmentUtility.class);
bind(String.class).annotatedWith(Names.named("LogPath")).toInstance("/home/moe/log/logger.log");

2. Bindings √ľber Annotationen

Die Einf√ľhrung einer @Binding -Annotation w√ľrde das definieren von Bindings vereinfachen. Diese w√ľrden bereits in den Klassen vorgenommen.

@Binding(to = {LoggingService.class})
public class SimpleLoggingService implements LoggingService {
    ...
}

@Binding
public class MessageFragmentUtility {
    ...
}