Dependency Injection mit Google Guice - JAXenter
Eclipse-Plug-in-Programmierung mit Google Guice

Dependency Injection mit Google Guice

Sven Efftinge

Der Begriff Dependency Injection (DI) schwirrt schon seit einigen Jahren in der Java-Welt umher und ist mittlerweile zumindest im Bereich Java-EE-Entwicklung zu einer Standardtechnik für die Komposition komplexer Softwaresysteme geworden. DI ist aber nicht nur für serverbasierte Softwaresysteme interessant, sondern löst eine wichtige Klasse von Standardproblemen in jedem Java-Projekt: Es ermöglicht dem Entwickler ausschließlich gegen Schnittstellen zu programmieren. Das sorgt für eine lose Kopplung zwischen den verschiedenen Komponenten, was das System sehr wartbar und vor allem testbar macht. In diesem Artikel möchte ich erklären, wie man DI, oder genauer das DI-Framework Guice, bei der Eclipse-Plug-in-Entwicklung verwenden kann.

Hinter dem Begriff Dependency Injection verbirgt sich ein sehr einfaches Prinzip: Komponenten besorgen sich die benötigten Dienste nicht selbst, sondern deklarieren nur, dass sie diese benötigen. Bei der Erzeugung einer Komponente muss dann der Erzeuger dafür sorgen, dass alle erforderlichen Dienste konfiguriert werden. Ich möchte das an einem kleinen Beispiel erläutern, das auch auf der Webseite von Guice [1] zu finden ist. Stellen Sie sich vor, Sie entwerfen einen Abrechnungsdienst um Pizzabestellungen zu verrechnen. Das Interface könnte wie in Listing 1 aussehen.

Listing 1
 public interface BillingService {

  /**
   * Attempts to charge the order to the credit card. Both successful and
   * failed transactions will be recorded.
   *
   * @return a receipt of the transaction. If the charge was successful, the
   *      receipt will be successful. Otherwise, the receipt will contain a
   *      decline note describing why the charge failed.
   */
  Receipt chargeOrder(PizzaOrder order, CreditCard creditCard);
}

Eine typische Implementierung sieht wie in Listing 2 aus.

Listing 2
public class RealBillingService implements BillingService {
  public Receipt chargeOrder(PizzaOrder order, CreditCard creditCard) {
    CreditCardProcessor processor = new PaypalCreditCardProcessor();
    TransactionLog transactionLog = new DatabaseTransactionLog();

    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());
    }
  }
}

Wie Sie sehen, werden für die Verrechnung einer Pizzabestellung zwei weitere Dienste benötigt. Ein so genannter CreditCardProcessor wird für die Abrechnung mit einem Kreditkartenunternehmen verwendet und ein TransactionLog, um alle stattfindenden Transaktionen zu protokollieren. Da der Code leider auch gleich die Erzeugung der benötigten Komponenten übernimmt, indem er Konstruktoren von konkreten Implementierungen der Dienste aufruft, wird die Klasse RealBillingService immer nur mit exakt dieser Konfiguration benutzt werden können. Auch ist der Lebenszyklus der verwendeten Komponenten damit unveränderbar: Sie werden erzeugt, sobald die Methode aufgerufen wird. Was tun, wenn für eine andere Installation eine andere Kreditgesellschaft angesprochen werden soll oder noch wichtiger: Wie kann die Komponente getestet werden, ohne dass ich das ganze System hochfahren und sogar externe Dienste benutzen muss?

Factories

Diese Fragen sind natürlich nicht neu und werden normalerweise mit der Empfehlung zur Verwendung von Factories beantwortet (Listing 3).

Listing 3
public class RealBillingService implements BillingService {
  public Receipt chargeOrder(PizzaOrder order, CreditCard creditCard) {
    CreditCardProcessor processor = CreditCardProcessorFactory.getInstance();
    TransactionLog transactionLog = TransactionLogFactory.getInstance();

    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());
    }
  }
}

Anstatt direkt den Konstruktor aufzurufen, wird eine Indirektion, die so genannte Fabrik-methode, eingeführt. Was genau diese Fabrikmethode tut, ist damit in einer anderen Klasse gekapselt und kann durch Veränderung von globalem Zustand gesteuert werden (Listing 4).

Listing 4
public class CreditCardProcessorFactory {
  
  private static CreditCardProcessor instance;
  
  public static void setInstance(CreditCardProcessor creditCardProcessor) {
    instance = creditCardProcessor;
  }

  public static CreditCardProcessor getInstance() {
    if (instance == null) {
      return new SquareCreditCardProcessor();
    }
    
    return instance;
  }
}

Wenn dann alle immer brav daran denken, den globalen Zustand sauber zu hinterlassen, niemand Multithreading benutzt, und Sie mit dem sehr geschwätzigen, unintuitiven Testcode aus Listing 5 kein Problem haben, funktioniert es soweit.

Listing 5
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();

  @Override public void setUp() {
    TransactionLogFactory.setInstance(transactionLog);
    CreditCardProcessorFactory.setInstance(creditCardProcessor);
  }

  @Override public void tearDown() {
    TransactionLogFactory.setInstance(null);
    CreditCardProcessorFactory.setInstance(null);
  }

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

    assertTrue(receipt.hasSuccessfulCharge());
    assertEquals(100, receipt.getAmountOfCharge());
    assertEquals(creditCard, creditCardProcessor.getCardOfOnlyCharge());
    assertEquals(100, creditCardProcessor.getAmountOfOnlyCharge());
    assertTrue(transactionLog.wasSuccessLogged());
  }
}
Geschrieben von
Sven Efftinge
Kommentare

Schreibe einen Kommentar

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