Nehmen wir an, im Rahmen der Software-Sanierung soll eine bestehende Klasse neu entwickelt werden. Dabei soll auch direkt die Schnittstelle (also das Interface) angepasst werden. Wird die Schnittstelle verändert, so sind alle Codestellen, die die Schnittstelle der ursprünglichen Klasse verwenden, nicht mehr mit der überarbeiteten Klasse kompatibel.

Angenommen es existiert eine Klasse „PaymentProvider“, die für die Kommunikation mit dem Zahlungsanbieter zuständig ist. Diese Klasse soll überarbeitet werden. Der folgende Code-Schnipsel zeigt einen Ausschnitt aus deiner Klasse:

public class PaymentProvider {

  // [...]

 public boolean doPayment(Payment payment) {
  // [...]
 }

 // [...]

}

Die Methode „doPayment“ wurde in der Vergangenheit immer wieder erweitert. Neben dem eigentlichen Ausführen einer Zahlung benachrichtigt sie inzwischen auch den Zahlungsempfänger via E-Mail. Aus diesem Grund soll sie in eine neue Klasse mit sauber abgegrenzten Methoden überführt werden.

Entwicklung der neuen PaymentProvider-Klasse

Die Neu-Entwicklung der Klasse PaymentProvider sieht wie folgt aus. Sie enthält nun separate Mehoden für die Ausführung der Zahlung und der Benachrichtigung der Zahlungsempfänger.

public class PaymentProvider {

  // [...]

  /**
   * Execute the payment by executing the transaction
   */
  public boolean executePayment(Payment payment) {
    // [...]
  }

  /**
   * Notify the recipients of the payment via e-mail
   */
  public boolean sendEmailNotification(Payment payment) {
    // [...]
  }

  // [...]

}

Die neue Klasse enthält somit die beiden Methoden „executePayment“ und „sendEmailNotification“. Codestellen, die die ursprüngliche Klasse verwendet haben, sind nun nicht mehr mit der neuen Klasse kompatibel, da sie noch davon ausgehen, dass die Klasse eine Methode „doPayment“ hat. Die Klasse „Checkout“ ist z.B. eine solche Klasse:

public class Checkout {

  // [...]

  private PaymentProvider paymentProvider;

  public Checkout() {
    this.paymentProvider = new PaymentProvider();
  }

  public void finalizeOrder() {
    // [...]
    Payment payment = new Payment(199.99);

    this.paymentProvider.doPayment(payment);

    // [...]
  }

  // [...]

}

Die neue Klasse und die alten Referenzen verheiraten

Um die Schnittstellen (Methoden) der neuen Klasse mit den vielen im Code verteilten Referenzen auf die ursprüngliche Klasse kompatibel zu machen, wird das sog. Adapter-Pattern verwendet. Dabei werden die inkompatiblen Schnittstellen kombinierbar gemacht.

[Referenzen z.B. in der Checkout-Klasse] <=> [Adapter] <=> [Schnittstellen der neuen PaymentProvider-Klasse]

Im ersten Schritt wird dafür ein Interface angelegt, das die Methoden der ursprünglichen PaymentProvider-Klasse beihaltet. Sie wird die Methoden vorgeben, die der zu entwickelte Adapter bereitstellen muss.

public interface OldPaymentProvider {
  public boolean doPayment(Payment payment);
}

Im nächsten Schritt wird die Adapter-Klasse angelegt. Wir nennen diese mal „PaymentProviderAdapter“. Sie implementiert das Interface „OldPaymentProvider“ und stellt somit die den alten Codestellen bekannte Methode „doPayment“ zur Verfügung.

public class PaymentProviderAdapter implements OldPaymentProvider {

  private PaymentProvider newPaymentProvider;

  public PaymentProviderAdapter(PaymentProvider newPaymentProvider) {
    this.newPaymentProvider = newPaymentProvider;
  }
  @Override
  public boolean doPayment(Payment payment) {
    return this.newPaymentProvider.executePayment()
      && this.newPaymentProvider.sendEmailNotification();
  }
}

Bei dem Interface „OldPaymentProvider“ und der Klasse „PaymentProviderAdapter“ handelt es sich um temporäre Interfaces/Klassen, die lediglich für die Übergangsphase verwendet werden. Sobald alle Code-Stellen auf die Schnittstellen der neuen PaymentProvider-Klasse umgestellt sind, können sie entfernt werden.

Umstellung des Checkouts auf den neuen PaymentProvider

Nachdem der Adapter erstellt wurde, kann die Klasse „Checkout“ umgestellt werden, damit sie den neuen PaymentProvider verwendet, aber noch mit der ihr bekannten, ursprünglichen Schnittstelle arbeiten kann.

public class Checkout {

  // [...]

  private OldPaymentProvider paymentProvider;

  public Checkout() {
    PaymentProvider newPaymentProvider = PaymentProvider();
    this.paymentProvider = new PaymentProviderAdapter(newPaymentProvider);
  }

  public void finalizeOrder() {
    // [...]
    Payment payment = new Payment(199.99);
    this.paymentProvider.doPayment(payment);
    // [...]
  }

  // [...]

}

Fazit

Was soll nun das Ganze? Warum stellen wir nicht auch einfach direkt die Klasse Checkout auf die Schnittstelle der neuen PaymentProvider-Klasse um? Wir sind doch am Sanieren und möchten keine halben Sachen machen.

Der Grund dafür ist, dass durch die Entwicklung des Adapters die vollständige Umstellung der Checkout-Klasse sowie der weiteren Klassen vorerst ausgeklammert werden kann. Es kann eine Klasse neu entwickelt werden, ohne große Eingriffe in andere Klassen vornehmen zu müssen. Würde die Klasse Checkout auch direkt an die neuen Schnittstellen der PaymentProvider-Klasse angepasst werden, würden auch dort noch weitere Sanierungsmaßnahmen anfallen. Es entsteht eine Kette von Refaktorisierungen, die dafür sorgen, dass die Software während der Sanierung lange Zeit nicht kompilierbar sein kann. Dies erschwert die Durchführung der Sanierung als Team, da durchgeführte Refaktorisierungen nicht schnell in den Hauptentwicklungszweig zurückgeführt werden können.

Wir möchten erreichen, dass die durchgeführten Sanierungsmaßnahmen klein genug sind, um schnell abgeschlossen zu werden. Diese Zwischenerfolge erhöhen die Motivation und nehmen die Angst davor, eine Refaktorisierung durchzuführen.

Kategorien: Aktuelle Themen

0 Kommentare

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert.