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:
class Shop: def __init__(self, gateway): self.gateway = gateway
def pay(self, order, amount): if self.gateway.charge(amount): print(order + ": bezahlt") else: print(order + ": abgelehnt")charge hat.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: def make_payment(self, currency, amount_in_cents, options): print(f"FastPay: {amount_in_cents} {currency}") if amount_in_cents > 100000: return "DECLINED" return "APPROVED"Sie macht dieselbe Arbeit, nur in anderer Form. Die Methode heißt make_payment, 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:
shop = Shop(FastPayClient())shop.pay("#1042", 19.99)AttributeError: 'FastPayClient' object has no attribute 'charge'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 hat eine Methode charge, genau das, was der Shop erwartet. 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: def __init__(self, client): self.client = client
def charge(self, amount): cents = round(amount * 100) status = self.client.make_payment("EUR", cents, {}) return status == "APPROVED"Der Shop bekommt den Adapter statt des Clients. Im Shop ändert sich nichts:
shop = Shop(FastPayAdapter(FastPayClient()))shop.pay("#1042", 19.99)shop.pay("#1043", 1299)FastPay: 1999 EUR#1042: bezahltFastPay: 129900 EUR#1043: abgelehntDer 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:
payments = FastPayAdapter(FastPayClient())print(payments.charge(19.99))
checkout = CheckoutFacade(Stock(), payments, Invoices(), Mailer())checkout.place_order("Wasserkocher", 24.90)Schritt für Schritt, in Worten
- Der Shop ruft charge(Betrag in Euro) auf. FastPayClient hat nur make_payment(Währung, Cent, Optionen), und der direkte Aufruf prallt ab.
- FastPayAdapter tritt dazwischen: Nach außen hat es charge, wie der Shop es erwartet, innen hält er den FastPay-Client.
- charge(19.99): Der Adapter rundet Euro auf Cent, 1999, und ruft make_payment mit "EUR" auf.
- FastPay antwortet "APPROVED", und der Adapter macht daraus True: die Sprache des Shops.
- CheckoutFacade bekommt das Lager, die Zahlungen (derselbe Adapter), die Rechnungen und die E-Mail.
- place_order: Zuerst reserviert das Lager den Wasserkocher.
- Dann die Zahlung, wieder über den Adapter: 24.90 € werden zu 2490 Cent.
- 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
- Python prüft keine Typen: Der Shop ruft
chargebei allem auf, was du ihm gibst. Der Adapter braucht nur eine Methodechargemit den richtigen Parametern und dem richtigen Ergebnis. Bei einem fremden Objekt kommt der Fehler erst bei der Zahlung (AttributeError), nicht beim Start, also ist ein Test wichtig. - Wenn du die Form aufschreiben willst, beschreibt
typing.Protocol„ein Objekt mit einer Methodecharge(amount) -> bool“, und ein Type-Checker (mypy) prüft das, bevor das Programm läuft.
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: def reserve(self, item): print("Lager: reserviere " + item) return True
def release(self, item): print("Lager: gebe frei " + item)
class Invoices: def issue(self, item, amount): print(f"Rechnung: {item}, {amount:.2f} €")
class Mailer: def send(self, text): print("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: def __init__(self, stock, payments, invoices, mailer): self.stock = stock self.payments = payments self.invoices = invoices self.mailer = mailer
def place_order(self, item, amount): if not self.stock.reserve(item): return False if not self.payments.charge(amount): self.stock.release(item) return False self.invoices.issue(item, amount) self.mailer.send("Bestätigung für " + item) return TrueDie Zahlungen in der Facade sind ein Objekt mit charge, 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:
payments = FastPayAdapter(FastPayClient())checkout = CheckoutFacade(Stock(), payments, Invoices(), Mailer())checkout.place_order("Wasserkocher", 24.90)Lager: reserviere WasserkocherFastPay: 2490 EURRechnung: Wasserkocher, 24.90 €E-Mail: Bestätigung für WasserkocherEine 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:
invoices = Invoices()invoices.issue("Wasserkocher", 24.90)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?
payments = FastPayAdapter(FastPayClient())checkout = CheckoutFacade(Stock(), payments, Invoices(), Mailer())checkout.place_order("Fernseher", 1299)Probier es aus
Finde den Fehler
shop = Shop(FastPayAdapter(FastPayClient()))shop.pay("#1042", 19.99)FastPay: 1998 EUR#1042: bezahltWähle die Zeile mit dem Fehler.
int rundet nicht, es schneidet den Nachkommateil ab. Und 19.99 in einem float ist nicht genau 19.99: Gleitkommazahlen werden binär gespeichert, und 19.99 * 100 ergibt 1998.9999999999998. Abgeschnitten sind das 1998. Die Lösung ist round(amount * 100): die nächste ganze Zahl, 1999. Ein Cent wirkt klein, ist aber bei jedem solchen Betrag falsch, und die Bücher des Shops und die von FastPay stimmen nicht überein.Genauer, für Neugierige
Probier es selbst aus:
print(19.99 * 100)1998.9999999999998Das round von Python rundet Hälften zur geraden Zahl: round(2.5) ist 2. Für Geld, bei dem die Hälfte nach oben geht, gibt es Decimal, das den Betrag genau hält:
print(round(2.5), round(3.5))
from decimal import Decimal, ROUND_HALF_UP
print((Decimal("0.125") * 100).quantize(Decimal("1"), rounding=ROUND_HALF_UP))2 413Die 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: def send(self, cents, currency): print(f"PayNow: {currency} {cents}") return 0Schreibe PayNowAdapter, sodass der Shop ohne Änderungen über PayNow zahlt:
shop = Shop(PayNowAdapter(PayNowApi()))shop.pay("#1042", 19.99)PayNow: EUR 1999#1042: bezahltWeitere Aufgaben
- Eine Bestellung stornieren. Füge der Facade cancel_order(item) hinzu: Das Lager gibt den Artikel frei, und der Kunde bekommt eine E-Mail „Bestellung storniert für …“.
- Ein Testanbieter. Schreibe ein
FakeGatewaymit einer Methodecharge, 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
Verwandte Lektionen
Abschnitt „Verwandte Lektionen“- Baut auf: dict und set · Entwurfsmuster · Strategy · Singleton und Dependency Injection
- Geht weiter in: Der Shop: welches Muster wohin · Hexagonale und Clean Architecture · Alte und fremde Systeme anbinden
- Aufgaben zum Thema: Entwurfsmuster