Assisted Inject

Veröffentlicht: 9. November 2011 von pmischke in Dependency Injection

Es geht weiter mit einem Dependency Injection Thema, das ich hier „Asssited Inject“ nenne. Ich kann nicht ausschließen, dass es noch weitere Namen für diese Technik gibt. Mir ist sie zuerst bei dem Guice Framework und eben diesem Namen über den Weg gelaufen:

http://code.google.com/p/google-guice/wiki/AssistedInject

Es geht um folgendes Problem:

  • Ich möchte mehrere Objekte eines Typs erzeugen.
  • Mein Typ hat Abhängigkeiten zu anderen Objekte, die über den Konstruktor vom DI Container gesetzt werden sollen. Diese Abhängigkeiten sind im einfachsten Fall für alle Objekte identisch (also Singleton Scope). Das muss nicht so sein, ist aber für das Assisted Inject irrelevant, also nehme ich den einfachsten Fall an.
  • Mein Typ hat daneben weiter Konstrutor-Argumente, die der Client des Objekts im Moment der Erzeugung spezifizieren soll. Diese Argumente (im folgenden Parameter genannt) sind wahrscheinlich für jedes Objekt verschieden. Dadurch entsteht hier der Bedarf, mehrere Objekte zu erzeugen, selbst wenn die Objekte keinen modifizierbaren Zustand besitzen.

Worin genau besteht das Problem? Nun ja, zum einen möchte ich natürlich, dass der DI Container die Objekte erzeugt. So kann er sich um die notwendigen Abhängigkeiten kümmern. Zum anderen sollen aber die Clients die Objekte erzeugen, um ihnen Parameter zum Zeitpunkte der Erzeugung mitgeben zu können. Wer soll also jetzt die Objekte erzeugen?

Das Pattern „Assisted Inject“ zur Lösung des Konflikts sieht wie folgt aus: Der DI Container erzeugt zunächst eine Factory für die Objekte. Diese Factory wird mit den Abhängigkeiten versehen, die später die Objekte erhalten sollen. Die Clients bekommen nun vom DI Container die Factory zur Verfügung gestellt. Zum gewünschten Zeitpunkt erzeugen die Clients dann die Objekte durch Aufruf einer Factory Methode. Die Factory Methode erhält die gewünschten Parameter und reicht diese zusammen mit den Abhängigkeiten – die der Factory bekannt sind – an den Konstruktor weiter.

Diese Lösung lässt sich leicht „von Hand“ programmieren und ist nicht von der Verwendung eines bestimmten DI Containers abhängig.

Es mag jedoch lästig erscheinen, die zusätzliche Factory zwischen Client und dem benötigtem Typ zu schreiben. Sie enthält im Prinzip nur „Boilerplate Code“ zum Durchreichen der Parameter und Abhängigkeiten. Wer Guice verwendet, bekommt dazu Hilfe angeboten. Eine Erweiterung zu Guice kann die Factory zur Laufzeit dynamisch generieren. Es genügt, ein Interface für die Factory Methode zu schreiben (siehe Link oben).

Auch für CDI existiert solch eine Erweiterung.

http://www.warski.org/blog/2010/12/improving-autofactoriesassisted-inject/

Der Autor dieser Erweiterung moniert in seinem Blog einen Mangel des vorgestellten Patterns. Im Konstruktor des verwendeten Typs sind Parameter und die echten Abhängigkeiten vermischt. Es ist nicht explizit mit OO Mitteln modelliert, was Abhängigkeiten und was Parameter sind. Daher muss man auch bei Verwendung einer der Erweiterung die Konstruktor-Parameter annotieren, die keine Abhängigkeiten sind. Ansonsten kann der DI-Container bzw. die Erweiterung diese nicht unterscheiden. Der Autor der CDI Erweiterung versucht diese Unschönheit durch kombinierte Verwendung von Kontruktor- und Feld-Injektion zu modellieren. Konstruktor-Injektion für die Paramter und Feld-Injektion für die Abhängigkeiten. Das ist in meinen Augen nicht unbedingt eine Verbesserung. Ich bin an sich kein Freund von Feld-Injektion. Und dann auch noch gemischt mit Konstrutor-Injektion scheint mir das ziemlich gekünstelt. Der Code wird dadurch nicht unbedingt verständlicher.

Ich möchte hier eine alternative Java Implementierung des Assited Inject Patterns vorstellen. Mir gefällt diese Implementierung aus zwei Gründen. Erstens ist der Unterschied zwischen den Abhängigkeiten und Parameter explizit mit OO Mitteln modelliert. Zweitens ist der Boilerplate Code etwas reduziert, dass vielleicht die Notwendigkeit für den Einsatz von Erweiterungen entfällt. Der Trick der Implementierung besteht in der Verwendung einer inneren Klasse.

public class PaymentFactory {

    private final CreditService creditService;

    @Inject
    public PaymentFactory(CreditService creditService) {
        this.creditService = creditService;
    }

    public class Instance implements Payment {

        public Instance(Date startDate, Money amount) {
            // ...
        }

        public void doSomething() {
            creditService.serviceCall();
        }

    }

}

public class Client {

    private final PaymentFactory paymentFactory;

    @Inject
    public Client(PaymentFactory paymentFactory) {
        this.paymentFactory = paymentFactory;
    }

    public void createPayment() {
        Money amount = null;
        Date startDate = null;

        Payment myPayment = paymentFactory.new Instance(startDate, amount);
    }

}

Da diese Lösung mit einer konkreten Factory ohne Interface arbeitet, kann man sich die Factory Mehtode sparen und die Java Syntax für Konstruktion von inneren Klassen verwenden. Unschön ist, dass sich die Factory nicht austauschen lässt (z.B. durch einen Mock für Tests) und dass alle Clients abhängig von der konkreten inneren Klasse sind. Wem das nicht gefällt kann auch ein Interface einziehen, muss dafür aber die Factory Methode ergänzen:

public class InnerClassPaymentFactory implements PaymentFactory {

    private final CreditService creditService;

    @Inject
    public InnerClassPaymentFactory(CreditService creditService) {
        this.creditService = creditService;
    }

    @Override
    public Payment create(Date startDate, Money amount) {
        return new Instance(startDate, amount);
    }

    public class Instance implements Payment {

        public Instance(Date startDate, Money amount) {
            // ...
        }

        public void doSomething() {
            creditService.serviceCall();
        }

    }

}

public class Client {

    private final PaymentFactory paymentFactory;

    @Inject
    public Client(PaymentFactory paymentFactory) {
        this.paymentFactory = paymentFactory;
    }

    public void createPayment() {
        Money amount = null;
        Date startDate = null;

        Payment myPayment = paymentFactory.create(startDate, amount);
    }

}

Beiden Lösungen ist gemein, dass der Unterschied zwischen Parametern und Abhängigkeiten explizit modelliert ist. Die Parameter sind die Konstruktor Argumente des betrachteten Typs (hier Payment). Die Abhängigkeiten tauchen nun als Abhängigkeiten der Factory auf. Das Pattern besteht ja genau in dem Trick, die Abhängigkeiten des Typs zu Abhängigkeiten der Factory zu machen. Die Regeln für Sichtbarkeit bei innere Klassen in Java erlauben es, dass die Instanzen des Typs Zugriff auf die Abhängigkeiten der Factory haben. Das Durchreichen der Abhängigkeiten entfällt.

Kommentare
  1. Christian Raschka sagt:

    Die Idee ist so einfach … aber so klasse. Das probiere ich direkt aus … Danke

Hinterlasse einen Kommentar