Google Guice: Gut getestet, modular und wartbar

Dependency Injection

Wenn Sie aber doch zu den Java-Entwicklern gehören, denen ein globaler Zustand, an dem verschiedene Komponenten gleichzeitig modifizieren, nicht so zusagt, finden Sie die Lösung aus Listing 6 vielleicht eleganter.

Listing 6
public class RealBillingService implements BillingService {
  private final CreditCardProcessor processor;
  private final TransactionLog transactionLog;

  public RealBillingService(CreditCardProcessor processor, 
      TransactionLog transactionLog) {
    this.processor = processor;
    this.transactionLog = transactionLog;
  }

  public Receipt chargeOrder(PizzaOrder order, CreditCard creditCard) {
    try {
      ChargeResult result = processor.charge(creditCard, order.getAmount());
      transactionLog.logChargeResult(result);

      return result.wasSuccessful()
          ? Receipt.forSuccessfulCharge(order.getAmount())
          : Receipt.forDeclinedCharge(result.getDeclineMessage());
     } catch (UnreachableException e) {
      transactionLog.logConnectException(e);
      return Receipt.forSystemFailure(e.getMessage());
    }
  }
}

Der RealBillingService arbeitet nun mit Dependency Injection. Nein, Sie benötigen kein weiteres Framework – das was Sie hier sehen, ist bereits Dependency Injection. Die Klasse hat keine statische Abhängigkeit mehr auf konkrete Implementierungen, und das Wichtigste: selbst die Erzeugung übernimmt sie nicht mehr selbst. Das muss jetzt von außen gemacht werden. Die Klasse deklariert lediglich, was sie benötigt um zu arbeiten, und überlässt dem Verwender die Entscheidung, welche konkreten Dienste reingereicht (injiziert) werden. Der Testcode in Listing 7 sieht dann auch gleich viel schöner aus und benutzt keinen globalen Zustand mehr.

Listing 7
public class RealBillingServiceTest extends TestCase {

  private final PizzaOrder order = new PizzaOrder(100);
  private final CreditCard creditCard = new CreditCard("1234", 11, 2010);

  private final InMemoryTransactionLog transactionLog = new InMemoryTransactionLog();
  private final FakeCreditCardProcessor creditCardProcessor = new FakeCreditCardProcessor();

  public void testSuccessfulCharge() {
    RealBillingService billingService
        = new RealBillingService(creditCardProcessor, transactionLog);
    Receipt receipt = billingService.chargeOrder(order, creditCard);

    assertTrue(receipt.hasSuccessfulCharge());
    assertEquals(100, receipt.getAmountOfCharge());
    assertEquals(creditCard, creditCardProcessor.getCardOfOnlyCharge());
    assertEquals(100, creditCardProcessor.getAmountOfOnlyCharge());
    assertTrue(transactionLog.wasSuccessLogged());
  }
}
Warum Google Guice?

Wenn das alles so einfach ist, stellt sich die Frage, warum DI-Container wie Guice oder Spring überhaupt nützlich sind. Ein reales Softwaresystem besteht typischerweise aus hunderten, wenn nicht tausenden Komponenten mit unzähligen Abhängigkeiten. Stellen Sie sich vor, Sie müssten diesen Abhängigkeitsgraphen mit Java modellieren, in dem Sie alle Komponenten in der richtigen Reihenfolge erzeugen und jeweils die abhängigen Komponenten bei der Erzeugung übergeben. Das ist natürlich nicht praktikabel und deshalb gibt es Frameworks wie Guice.

Um die Komponenten aus dem Beispiel mit Guice zu nutzen, muss lediglich die Annotation @Inject an den Konstruktor gesetzt werden (Listing 8).

Listing 8
public class RealBillingService implements BillingService {
  private final CreditCardProcessor processor;
  private final TransactionLog transactionLog;

  @Inject
  public RealBillingService(CreditCardProcessor processor,
      TransactionLog transactionLog) {
    this.processor = processor;
    this.transactionLog = transactionLog;
  }

  public Receipt chargeOrder(PizzaOrder order, CreditCard creditCard) {
    try {
      ChargeResult result = processor.charge(creditCard, order.getAmount());
      transactionLog.logChargeResult(result);

      return result.wasSuccessful()
          ? Receipt.forSuccessfulCharge(order.getAmount())
          : Receipt.forDeclinedCharge(result.getDeclineMessage());
     } catch (UnreachableException e) {
      transactionLog.logConnectException(e);
      return Receipt.forSystemFailure(e.getMessage());
    }
  }
}

Wenn man sich nun einen so genannten Injector erzeugt, kann man diesen nach Instanzen des BillingService fragen (Listing 9).

Listing 9
public static void main(String[] args) {
  Injector injector = Guice.createInjector(new BillingModule());
  BillingService billingService = injector.getInstance(BillingService.class);
  ...
}

Welche konkrete Implementierung von Guice erzeugt werden soll, wie und wie oft dies geschieht, wird in einem so genannten Module konfiguriert. Hier bietet Guice ein gut lesbares Fluent-API an. Die Konfiguration für den Produktionsmodus unseres Beispiels würde wie in Listing 10 aussehen.

Listing 10
public class BillingModule extends AbstractModule {
  @Override 
  protected void configure() {
    bind(TransactionLog.class).to(DatabaseTransactionLog.class);
    bind(CreditCardProcessor.class).to(PaypalCreditCardProcessor.class);
    bind(BillingService.class).to(RealBillingService.class);
  }
}

Dienste werden in Guice immer über einen so genannten Key identifiziert, wobei dieser immer einen Typen beinhaltet (z. B. BillingService), aber zusätzlich noch mit einer Annotation versehen werden kann, falls es zwei verschiedene Komponenten des gleichen Typs geben soll. Mit diesem Programmiermodell lassen sich beliebig große Systeme sehr leicht entkoppeln und vor allem wird es sehr einfach, die einzelnen Komponenten mit kleinen Unit Tests zu testen. Guice bietet über diese einfachen Mechanismen hinaus natürlich noch eine Reihe fortschrittlicher Konzepte und sehr bequeme Defaults an. Das Grundprinzip ist aber hiermit erklärt.

Kommentare

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht.