Dependency Injection mit der Reader-Monade

Schon wieder nehme ich das böse M-Wort in den Mund! Dabei ist die Reader-Monade kaum mehr als eine einfache Function. Aber eins nach dem anderen.

Zuerst einmal das Datenmodell mit einem Fake-Service:

public class User {
    public long id;
    public String name;

    public User(String name, long id) {
        this.id = id;
        this.name = name;
    }
}

public interface UserService {
    User getUser(long id);

    List<User> findAll();
}

public class UserServiceImpl implements UserService {
    @Override
    public User getUser(long id) {
        return new User("user" + id,id);
    }

    @Override
    public List<User> findAll() {
        return IntStream.of(1,2,5,42).
               mapToObj(this::getUser).
               collect(Collectors.toList());
    }
}

Nun das Grundgerüst der Komponente, die diesen Service gern benutzen würde:

public class UserComponent {
    public User getUser(long id) { ??? }
    public String greet(User user) { ???  }
    public List<User> getAllUsers() { ??? }
    public String greetAll() { ??? }
}

Ohne den UserService läuft natürlich nichts, und wir wollen ihn auch nicht mit gewöhnlichen DI-Frameworks hineinzaubern. Welche Möglichkeiten haben wir dann?

Am naheliegendsten ist sicher, den Service im Konstruktor mitzugeben und in einer Instanzvariable zu speichern. Das Problem dabei ist, dass dann nur dort Objekte konstruiert werden können, wo der „richtige“ Service bekannt ist. Andere benötigte Klassen, die ebenfalls den Service benötigen, müssen als weitere Parameter übergeben werden (was die Initialisierung verkompliziert), oder im Konstruktor initialisiert werden (was zu unschönen Abhängigkeiten führt).

Weiterhin könnte man einfach jeder Methode den Service als Parameter mitgeben, also UserComponent.getUser(long id, UserService service). Das funktioniert, wird aber schnell ziemlich unschön, da dieser Parameter bei jedem einzelnen Aufruf „mitgeschleift“ werden muss.

Wir haben auch eine weitere Möglichkeit, nämlich keinen Wert, sondern eine Funktion zurückzuliefern, als wenn wir sagen wollten: Wenn du uns einen UserService gibst, können wir dir den Wert berechnen: Function getUser(long id).

Mit etwas „Verfeinerung“ ist das die Idee der Reader-Monade. Als erstes stellt sich die Frage, was ist, wenn wir mehr Services oder andere Daten (etwa aus einer Property-Datei) brauchen. Deshalb bündeln wir das Ganze gleich in ein Config-Interface:

public interface Config {
    public UserService userService();
    //später mehr...
}

Dann fällt uns auf, dass alle Methoden den Typ Function<Config, Irgendwas> zurückgeben würden. Da sollten wir uns Schreibarbeit sparen, und diesem neuen Typ gleichzeitig ein paar nützliche Methoden (von denen zwei, nämlich pure und flatMap, das Ding auch formal zu einer Monade machen) verpassen:

//R wie "Reader"
public interface R<A> extends Function<Config, A> {

    @Override
    A apply(Config c);

    static <A> R<A>; pure(A a) {
        return s -> a;
    }

    default <B> R<B> map(Function<A, B> fn) {
        return s -> fn.apply(apply(s));
    }

    default <B> R<B> flatMap(Function<A, R<B>> fn) {
        return s -> fn.apply(apply(s)).apply(s);
    }
}

Damit würde unsere UserComponent so aussehen:

public class UserComponent {

    public R<User> getUser(long id) {
        return config -> config.userService().getUser(id);
    }

    public R<String> greet(User user) {
        return R.pure("Hello " + user.name + "!");
    }

    public R<List<User>> getAllUsers() {
        return config -> config.userService().findAll();
    }

    public R<String> greetAll() {
        return getAllUsers().map(list ->
                list.stream().map(user ->
                        "Hello " + user.name + "!\n").
                        reduce("", String::concat));
    }

}

OK, das sieht erst einmal ziemlich gewöhnungsbedürftig aus. getUser und getAllUsers sind einfach zu verstehen, sie reichen nur die Aufrufe an den Service weiter. Die Methode greet könnte eigentlich ohne Service auskommen, aber jeder weiß, wie schnell sich das ändern kann, und wer will sich schon merken, welche Methode jetzt eine Config braucht und welche nicht? Deshalb wird mit pure einfach ein R erzeugt, das einen Wert zurückliefert und dabei die Config völlig ignoriert. In greetAll sieht man, wie die Methoden aufeinander aufbauen können, in dem man sie mit map- (oder auch flatMap-) Aufrufen miteinander verknüppert. Interessant ist, dass hier überhaupt keine Spur mehr von einem Service oder einem Config-Objekt zu sehen ist, außer dem R-Rückgabetyp.

Wie wird das Ganze nun benutzt? Zuerst einmal braucht man natürlich eine Implementierung von Config. In unserem Mini-Beispiel reicht eine anonyme Klasse:

Config config = new Config() {
    @Override
    public UserService userService() {
        return new UserServiceImpl();
    }
};
...

Dann kann man Methoden wie gewohnt aufrufen, nur muss man überall ein .apply(config) dranhängen:

...
UserComponent userComp = new UserComponent();
System.out.println(userComp.greetAll().apply(config));
...

Natürlich können auch hier einzelne Methoden miteinander verknüpft werden:

...
System.out.println(userComp.getUser(42).
                   flatMap(userComp::greet).apply(config));
...

Das war das Grundprinzip der Reader-Monade als DI-Ersatz. Ich gebe zu, der entstehende Code sieht erst einmal ziemlich ungewöhnlich aus, und ich bin skeptisch, wie gut das Ganze in größeren Systemen funktionieren würde. Trotzdem ist es interessant zu sehen, wie man sich mit „Bordmitteln“ behelfen kann, wenn man kein DI-Framework einsetzen will.

Die Kraft des Saftes: Dependency Injection

Theoretisch waren mir die Vorteile von Dependency Injection schon länger klar, aber der Einsatz in der Praxis ist eben doch etwas anderes, so ungefähr wie wenn man das erste Mal einen Fahrradhelm aufzieht: Es ist ungewohnt und etwas unbequem, aber man hat das Gefühl, dass man das Richtige tut…

Und so habe ich denn begonnen, bei einem kleineren, lokalen Java-Projekt ohne harte Deadline von Anfang an Google Guice zu verwenden. Von den vorhandenen Alternativen schien es mir die leichtgewichtigste zu sein, und auch seine auf Annotations basierende Architektur sagte mir zu. Trotzdem musste ich gewaltig umdenken und auch ein paar (schlechte?) Gewohnheiten über Bord werfen.

Die Konfiguration der Klassen, die DI benötigen, erfolgt bei Guice über sogenannte Module. Diese Module werden dem sogenannten Injector übergeben, der dann das Injizieren übernimmt. Normalerweise geschieht das nur einmal in einer zentralen Klasse: Der Injektor konstruiert einem die benötigten Objekte mit den in den Modulen spezifizierten (oder „gebundenen“) Konstruktor-Parametern. Der Witz ist, dass diese Konstruktor-Parameter selber wieder Parameter injiziert bekommen können und so weiter und so fort. Konstruktor-Injektion ist also „ansteckend“, und erst dadurch skaliert das ganze Konzept – es wäre furchtbar unpraktisch und würde der Idee von DI widersprechen, wenn man stattdessen überall den Injector herumreichen müsste. Guice unterstützt auch andere Formen der Injektion (etwa „nachträgliche“ Injektion bei Objekten, die aus Factories stammen), aber darauf will ich hier nicht eingehen.

Eine gute Idee ist, jedem Package sein eigenes Modul zu spendieren, denn dann kann man die Implementierungen für ein Interface package-private machen (so sie denn im gleichen Package stehen). Zuerst war ich skeptisch, weil man ja dem Injector alle Module „hartkodiert“ übergeben muss, aber es fand sich (hier) eine elegante Lösung: Der ServiceLoader von Javas eingebauten SPI-Mechanismus. Das sieht dann etwa so aus:

public static void main(String... args) {
    final Injector injector = Guice.createInjector(
        ServiceLoader.load(com.google.inject.Module.class));
    injector.getInstance(MeineHauptklasse.class);
}

Die einzelnen Module listet man dann einfach in einer Text-Datei META_INF/services/com.google.inject.Module auf (eine ausführliche Beschreibung von SPI findet sich hier).

Die Idee, ServiceLoader mit Guice zu koppeln, lässt sich auch anderswo verwenden. In meiner Applikation habe ich mehrere, voneinander weitgehend unabhängige Tabs, und es wäre natürlich schön, diese dynamisch einbinden zu können, ähnlich wie Plugins. Auch das funktioniert wie geschmiert. Zuerst benötigen wir ein gemeinsames Interface und Implementierungen:

public interface TabProvider {
   public JPanel getPanel();
}
 
public class FooTabProvider implements TabProvider {
   public JPanel getPanel(){...};
}
 
public class BarTabProvider implements TabProvider {
   public JPanel getPanel(){...};
}

Nun verknoten wir das in einem Guice-Modul mit einen ServiceLoader. Die magische Zutat, mit der man gleich ein Set von Werten injizieren kann, heißt „Multibinder“:

public GuiModule implements com.google.inject.AbstractModule {
    @Override
    protected void configure() {
        Multibinder<TabProvider> tabBinder = Multibinder.newSetBinder(binder(), TabProvider.class);
        for(TabProvider tabProvider : ServiceLoader.load(TabProvider.class)) {
            tabBinder.addBinding().toInstance(tabProvider);
        }
    }
}

Natürlich müssen jetzt die einzelnen Module wie vorhin in einer Text-Datei mit dem Namen des Interfaces, also z.B. META-INF/services/my.package.TabProvider, aufgelistet werden. Nun können alle TabProvider an der richtigen Stelle als Set injiziert werden:

 
public class MyMainFrame {
  @Inject
  public MyMainFrame(Set<TabProvider> tabProviders) {
     ...
  }
  ...
}

Ein Set garantiert natürlich keine Reihenfolge, aber ich habe mich beholfen, indem ich die Implementierungen einfach die Tab-Position als Zahl zurückliefern lasse. Wenn man nach alter BASIC-Manier die Positionen in Zehnerschritten wählt, kann man später immer noch andere Tabs dazwischen einfügen.

Eine andere nützliche Methode, um die Bindungen in den Modulen über eine Datei konfigurieren zu können, ist die Verwendung der guten alten Properties-Dateien:

public MyModule extends AbstractModule {
    @Override
    protected void configure() {
       try {
          Properties properties = new Properties();
          properties.load(new FileReader("my.properties"));
          Names.bindProperties(binder(), properties) 
       } catch(Exception ex) {
           ...  
       } 
   } 
}

Danach kann man die Properties wie folgt injizieren lassen:

public class Client {
   @Inject
   public Client(@Named("url") String url) {
      ...
   }
} 

Das soll es erst einmal für heute mit Guice und Java gewesen sein. Es drängt sich natürlich die Frage auf: Und was ist mit Scala? Ich kann mir vorstellen, dass es Situation gibt, in denen Guice auch in Scala vorteilhaft wäre, aber in den meisten Fällen sind Scalas „Bordmittel“ meiner Meinung nach ausreichend. Glücklicherweise brauche ich darüber nicht viel zu schrieben, denn dieser exzellente Artikel von Jonas Bonér listet verschiedene Möglichkeiten auf und diskutiert ihre Vor- und Nachteile. Ein Gebiet, auf dem meiner Meinung nach Guice glänzen könnte, wäre die Kopplung von Scala und Java: Ein Guice-Modul ist ein guter Platz, um Häßlichkeiten (man denke an $MODULE oder varargs) beim Aufruf der jeweils fremden Sprache zu verstecken.