Zum Inhalt
wannacode

Adapter und Facade

Kurz gesagt: Ein Adapter übersetzt eine fremde Schnittstelle in die, die dein Code erwartet. Eine Facade versteckt mehrere Subsysteme (Lager, Zahlung, Rechnung, E-Mail) hinter einer einzigen Methode.

Problem

Der neue Anbieter passt nicht

Der Shop zahlt über seine eigene Schnittstelle: Charge nimmt den Betrag in Euro und sagt, ob die Zahlung durchgegangen ist. Der Shop weiß nicht, wer dahintersteckt:

interface IPaymentGateway
{
bool Charge(decimal amount);
}
class Shop
{
private readonly IPaymentGateway gateway;
public Shop(IPaymentGateway gateway)
{
this.gateway = gateway;
}
public void Pay(string order, decimal amount)
{
if (gateway.Charge(amount))
{
Console.WriteLine(order + ": bezahlt");
}
else
{
Console.WriteLine(order + ": abgelehnt");
}
}
}

Die Firma wechselt zu FastPay, das niedrigere Gebühren verlangt. FastPay liefert eine fertige Bibliothek:

// Die FastPay-Bibliothek: fremder Code, den du nicht ändern kannst.
class FastPayClient
{
public string MakePayment(string currency, long amountInCents, Dictionary<string, string> options)
{
Console.WriteLine($"FastPay: {amountInCents} {currency}");
if (amountInCents > 100000)
{
return "DECLINED";
}
return "APPROVED";
}
}

Sie macht dieselbe Arbeit, nur in anderer Form. Die Methode heißt MakePayment, will eine Währung, einen Betrag in Cent und Optionen und gibt den Text "APPROVED" oder "DECLINED" statt Ja oder Nein zurück. Direkt an den Shop übergeben, passt sie nicht:

var shop = new Shop(new FastPayClient());
Fehler
error CS1503: Argument 1: cannot convert from 'FastPayClient' to 'IPaymentGateway'

Die Bibliothek ist fremder Code: Du kannst ihr keine Methode hinzufügen. Den Shop für FastPay umzuschreiben heißt, jede Stelle anzufassen, die zahlt, und sie beim nächsten Anbieter noch einmal anzufassen.

Die Idee

Zwei Hüllen, zwei verschiedene Ziele

Beide Muster dieser Lektion umhüllen fremden Code, lösen aber verschiedene Probleme.

Ein Adapter ist eine Klasse mit der Form, die dein Code erwartet, und ruft im Inneren das fremde Objekt auf. Er übersetzt die Namen, die Einheiten und das Ergebnis. Wie ein Reisestecker: Der Stecker deines Ladegeräts passt im Ausland nicht in die Steckdose, der Adapter passt zu beiden. Er fügt nichts hinzu, er ändert nur die Form.

Eine Facade ist ein einfacher Einstiegspunkt vor vielen Klassen, die in einer bestimmten Reihenfolge benutzt werden müssen. Wie die Rezeption im Hotel: Du rufst nicht einzeln die Reinigung, die Küche und die Buchhaltung an, du sagst es der Rezeption.

Der Unterschied in Kürze:

  • ein Adapter ändert die Form einer Schnittstelle, damit sie zu einer anderen passt;
  • eine Facade vereinfacht die Arbeit mit vielen Klassen, indem sie die übliche Abfolge hinter eine Methode legt.

Sieh es dir an

Der Adapter übersetzt

FastPayAdapter implementiert IPaymentGateway. Im Inneren hält er den FastPay-Client und übersetzt: Euro in Cent, "EUR" und leere Optionen, den Text in Ja oder Nein:

class FastPayAdapter : IPaymentGateway
{
private readonly FastPayClient client;
public FastPayAdapter(FastPayClient client)
{
this.client = client;
}
public bool Charge(decimal amount)
{
long cents = (long)Math.Round(amount * 100, MidpointRounding.AwayFromZero);
string status = client.MakePayment("EUR", cents, new Dictionary<string, string>());
return status == "APPROVED";
}
}

Der Shop bekommt den Adapter statt des Clients. Im Shop ändert sich nichts:

var shop = new Shop(new FastPayAdapter(new FastPayClient()));
shop.Pay("#1042", 19.99m);
shop.Pay("#1043", 1299m);
Ausgabe
FastPay: 1999 EUR
#1042: bezahlt
FastPay: 129900 EUR
#1043: abgelehnt

Der Fernseher für 1299 € liegt über dem Limit von FastPay (1000 €), also wird er abgelehnt. Der Shop erfährt es in seiner eigenen Sprache: false.

Die Szene zeigt den Adapter und danach die Facade von der nächsten Karte:

var payments = new FastPayAdapter(new FastPayClient());
Console.WriteLine(payments.Charge(19.99m));
var checkout = new CheckoutFacade(new Stock(), payments, new Invoices(), new Mailer());
checkout.PlaceOrder("Wasserkocher", 24.90m);
Schritt für Schritt, in Worten
  1. Der Shop ruft Charge(Betrag in Euro) auf. FastPayClient hat nur MakePayment(Währung, Cent, Optionen), und der direkte Aufruf prallt ab.
  2. FastPayAdapter tritt dazwischen: Nach außen ist es IPaymentGateway mit Charge, innen hält er den FastPay-Client.
  3. Charge(19.99): Der Adapter rundet Euro auf Cent, 1999, und ruft MakePayment mit "EUR" auf.
  4. FastPay antwortet "APPROVED", und der Adapter macht daraus True: die Sprache des Shops.
  5. CheckoutFacade bekommt das Lager, die Zahlungen (derselbe Adapter), die Rechnungen und die E-Mail.
  6. PlaceOrder: Zuerst reserviert das Lager den Wasserkocher.
  7. Dann die Zahlung, wieder über den Adapter: 24.90 € werden zu 2490 Cent.
  8. Die Zahlung ist durch, also stellt die Facade die Rechnung aus und schickt die E-Mail. Der Aufrufer hat eine Zeile geschrieben.
Genauer, für Neugierige
  • Auch .NET hat Adapter. StreamReader macht aus einem Byte-Strom (Stream) einen Textleser, und ReadOnlyCollection<T> zeigt eine Liste als schreibgeschützte Collection.
  • Der Adapter hält das fremde Objekt in einem Feld (Komposition). Er könnte auch von ihm erben, aber dann sieht der Shop auch jede FastPay-Methode, und ein Adapter kann keinen Client umhüllen, der schon existiert.

Sieh es dir an

Die Facade vor vier Klassen

Die Zahlung ist nur ein Schritt einer Bestellung. Davor reserviert das Lager den Artikel, danach kommen die Rechnung und die E-Mail. Wird die Zahlung abgelehnt, gibt das Lager den Artikel wieder frei. Jeder Schritt ist eine kleine Klasse:

class Stock
{
public bool Reserve(string item)
{
Console.WriteLine("Lager: reserviere " + item);
return true;
}
public void Release(string item)
{
Console.WriteLine("Lager: gebe frei " + item);
}
}
class Invoices
{
public void Issue(string item, decimal amount)
{
Console.WriteLine($"Rechnung: {item}, {amount:F2} €");
}
}
class Mailer
{
public void Send(string text)
{
Console.WriteLine("E-Mail: " + text);
}
}

Ohne Facade schreibt jede Stelle, die eine Bestellung annimmt (die Website, die Mobil-App, die telefonischen Bestellungen), dasselbe in derselben Reihenfolge:

if (stock.Reserve(item))
{
if (payments.Charge(amount))
{
invoices.Issue(item, amount);
mailer.Send("Bestätigung für " + item);
}
else
{
stock.Release(item);
}
}

Vergisst eine Stelle stock.Release, bleibt der Artikel für eine Bestellung reserviert, die nie bezahlt wurde. CheckoutFacade schreibt die Reihenfolge einmal auf:

class CheckoutFacade
{
private readonly Stock stock;
private readonly IPaymentGateway payments;
private readonly Invoices invoices;
private readonly Mailer mailer;
public CheckoutFacade(Stock stock, IPaymentGateway payments, Invoices invoices, Mailer mailer)
{
this.stock = stock;
this.payments = payments;
this.invoices = invoices;
this.mailer = mailer;
}
public bool PlaceOrder(string item, decimal amount)
{
if (!stock.Reserve(item))
{
return false;
}
if (!payments.Charge(amount))
{
stock.Release(item);
return false;
}
invoices.Issue(item, amount);
mailer.Send("Bestätigung für " + item);
return true;
}
}

Die Zahlungen in der Facade sind ein IPaymentGateway, von außen hineingereicht (Dependency Injection aus der Lektion „Singleton und Dependency Injection“). Hier ist es der Adapter von der vorigen Karte. Die beiden Muster arbeiten zusammen:

var payments = new FastPayAdapter(new FastPayClient());
var checkout = new CheckoutFacade(new Stock(), payments, new Invoices(), new Mailer());
checkout.PlaceOrder("Wasserkocher", 24.90m);
Ausgabe
Lager: reserviere Wasserkocher
FastPay: 2490 EUR
Rechnung: Wasserkocher, 24.90 €
E-Mail: Bestätigung für Wasserkocher

Eine Facade ist keine Mauer. Die Klassen dahinter kannst du weiter direkt benutzen, wenn du etwas abseits des üblichen Wegs brauchst. Die Buchhaltung stellt die Rechnung noch einmal aus:

var invoices = new Invoices();
invoices.Issue("Wasserkocher", 24.90m);
Ausgabe
Rechnung: Wasserkocher, 24.90 €

Probier es aus

Sag die Ausgabe voraus

Die Klassen stammen aus den Karten „Der Adapter übersetzt“ und „Die Facade vor vier Klassen“. Was gibt das Programm aus?

var payments = new FastPayAdapter(new FastPayClient());
var checkout = new CheckoutFacade(new Stock(), payments, new Invoices(), new Mailer());
checkout.PlaceOrder("Fernseher", 1299m);
Antwort

Probier es aus

Finde den Fehler

Ein Kollege hat den Adapter „vereinfacht“: Cent sind ja ohnehin eine ganze Zahl. Der Shop verkauft auch Kaffee nach Gewicht. Ein Kunde kauft 375 g zu 13,99 € pro Kilogramm (Bestellung #1044):
var shop = new Shop(new FastPayAdapter(new FastPayClient()));
shop.Pay("#1044", 0.375m * 13.99m);
Ausgabe
FastPay: 524 EUR
#1044: bezahlt
375 g zu 13,99 € sind 5,24625 €, also 5,25 €. FastPay hat 524 Cent genommen, einen zu wenig. In welcher Zeile des Adapters steckt der Fehler?

Wähle die Zeile mit dem Fehler.

Genauer, für Neugierige

Probier es selbst aus:

Console.WriteLine(0.375m * 13.99m * 100);
Ausgabe
524.62500
Deshalb ist Geld in C# decimal und nicht double: Mit double ergibt 19.99 * 100 wie in Java 1998.9999999999998. decimal behält die Dezimalstellen genau, aber Bruchteile eines Cents müssen trotzdem gerundet werden, und die Regel (MidpointRounding) schreibst du ausdrücklich hin.

Die Idee

Wann du sie nicht brauchst

Einen Adapter brauchst du nicht, wenn beide Seiten dein eigener Code sind. Ändere dann die eine Seite, damit sie zur anderen passt, statt einen Übersetzer dazwischenzusetzen. Ein Adapter ist für Code, den du nicht anfassen kannst: eine fremde Bibliothek, ein altes System, den Dienst eines anderen Teams.

Eine Facade brauchst du nicht, wenn die Abfolge an einer einzigen Stelle benutzt wird. Dort reicht eine Funktion. Eine Facade hat auch eine Falle: Sie wird leicht zur Klasse, durch die alles läuft, mit einer Methode für jeden Wunsch jedes Bildschirms. Halte nur die üblichen Wege in ihr und rufe die Klassen für alles andere direkt auf.

Beide Muster sind dünn. Ein Adapter, der anfängt, Geschäftsregeln zu entscheiden (Rabatte, Limits), ist kein Adapter mehr. Eine Facade, die Beträge selbst berechnet, statt die Klassen aufzurufen, die das tun, auch nicht.

Übung

Ein zweiter Anbieter

Löse es in deinem eigenen Editor.

PayNow. Die Firma nimmt einen zweiten Anbieter dazu, PayNow. Seine Bibliothek will die Cent zuerst und die Währung danach und gibt eine Zahl zurück: 0 heißt Erfolg, alles andere ist eine Ablehnung.

class PayNowApi
{
public int Send(int cents, string currency)
{
Console.WriteLine($"PayNow: {currency} {cents}");
return 0;
}
}

Schreibe PayNowAdapter, sodass der Shop ohne Änderungen über PayNow zahlt:

var shop = new Shop(new PayNowAdapter(new PayNowApi()));
shop.Pay("#1042", 19.99m);
Ausgabe
PayNow: EUR 1999
#1042: bezahlt

Weitere Aufgaben

  • Eine Bestellung stornieren. Füge der Facade CancelOrder(item) hinzu: Das Lager gibt den Artikel frei, und der Kunde bekommt eine E-Mail „Bestellung storniert für …“.
  • Ein Testanbieter. Schreibe FakeGateway : IPaymentGateway, das jeden Betrag über 50 € ablehnt. Gib es der Facade und prüfe, dass das Lager bei einer Ablehnung den Artikel freigibt.
  • Die Rechnung schlägt fehl. Lass das Ausstellen der Rechnung eine Exception werfen. Das Geld ist schon abgebucht. Was soll die Facade tun? Entscheide und schreibe es auf: zum Beispiel mit der E-Mail weitermachen und den Fehler festhalten, damit die Rechnung später ausgestellt wird.

Test

Teste dich

1. Die Bibliothek des Anbieters hat eine Methode mit anderem Namen, anderen Einheiten und anderem Ergebnis, als dein Code erwartet. Welches Muster passt?
2. Die Website, die Mobil-App und die telefonischen Bestellungen wiederholen dieselben vier Schritte in derselben Reihenfolge. Welches Muster passt?
3. Kann man mit einer Facade das Lager und die Rechnungen noch direkt benutzen?
4. Der Adapter macht aus 19,99 € Cent, indem er den Nachkommateil abschneidet. Was bekommt der Anbieter?

Erstellt von Ivo Gergov im Rahmen der Kampagne von Evo Tech für zugängliche Programmierausbildung ·