Be Software Architecture Wise
Be Wise Apps

Read in English · Leer en español · Lire en français

Abschnitt 3.3 von 3.8 · Mittelstufe · 24 Min. Lesezeit

Information Hiding, Abstraktion und Schnittstellen

Videolektion

5 Min. · auf Englisch gesprochen, mit Untertiteln
Nur anhören (Audio)
Transkript lesen

Jedes gute Modul hat ein Geheimnis. In dieser Lektion erfahren Sie, was Information Hiding wirklich bedeutet, wie Schnittstellen als Verträge funktionieren, warum Abstraktionen undicht sind und wie Sie die Schnittstellen zwischen Komponenten stabil halten.

Wenn Sie ein Paket aufgeben, geben Sie es am Schalter ab und erhalten eine Sendungsnummer. Sie wissen nicht, und es ist Ihnen egal, ob es per Transporter, Zug oder Flugzeug reist oder durch welches Depot es läuft. Die Post kann all das morgen ändern, weil die Vereinbarung am Schalter dieselbe bleibt. Gute Module funktionieren wie dieser Schalter. Ärger beginnt, wenn sich Kunden auf Details verlassen, die niemand versprochen hat, etwa dass das Paket immer vor zehn ankommt.

Information Hiding ist das Prinzip, das David Parnas 1972 vorgeschlagen hat. Jedes Modul verbirgt eine Designentscheidung, die schwierig ist oder sich wahrscheinlich ändert, hinter einer Schnittstelle, die so wenig wie möglich preisgibt. Gute Kandidaten zum Verbergen sind Datenstrukturen, die Reihenfolge von Operationen, Ein- und Ausgabeformate, Algorithmen und heute auch, welchen Lieferanten oder welche Datenbank Sie nutzen. Was verborgen ist, lässt sich günstig ändern. Was offenliegt, wird teuer zu ändern, weil andere davon abhängen.

Verwechseln Sie das nicht mit Kapselung. Kapselung ist ein Sprachmechanismus, etwa private Felder. Information Hiding ist die Designentscheidung darüber, was geheim bleibt. Man kann perfekt kapseln und trotzdem nichts verbergen. Eine Klasse mit privaten Feldern und je einem öffentlichen Getter und Setter verrät jedem Aufrufer, welche Felder sie hat. Ändern Sie sie, und jeder Aufrufer bricht. Eine Schnittstelle ist ein Vertrag, und Bertrand Meyers Design by Contract macht ihn präzise. Vorbedingungen sind das, was der Aufrufer erfüllen muss. Nachbedingungen sind das, was die Operation dann garantiert. Invarianten gelten zwischen allen Operationen. Der Vertrag weist auch die Schuld zu. Ist eine Vorbedingung verletzt, liegt der Fehler beim Aufrufer. Ist eine Nachbedingung verletzt, liegt er beim Anbieter.

John Ousterhout bietet ein nützliches Bild. Stellen Sie sich ein Modul als Rechteck vor. Die obere Kante ist die Schnittstelle, die alle lernen müssen. Die Fläche ist die Funktionalität, die es bietet. Ein tiefes Modul hat eine kleine Schnittstelle über viel Funktionalität. Die Dateiverarbeitung unter Unix ist sein Lieblingsbeispiel: open, read, write, seek und close verbergen Caching, Geräte, Berechtigungen und Plattenlayout. Ein flaches Modul hat eine Schnittstelle, die fast so komplex ist wie das, was es tut, etwa eine Wrapper-Klasse, deren Methoden je einen Aufruf direkt durchreichen. Es verursacht Kosten, ohne viel zu verbergen. Das ist auch für Services ein wertvoller Test. Ein Service, dessen API einfach seine Datenbanktabellen abbildet, ist flach und schiebt die gesamte eigentliche Logik auf seine Clients ab.

2002 schrieb Joel Spolsky, dass alle nicht trivialen Abstraktionen bis zu einem gewissen Grad undicht sind: Ihre verborgenen Details betreffen Sie trotzdem. Eine Datenbank-Mapping-Bibliothek verbirgt SQL, bis eine Schleife stillschweigend eine Abfrage pro Zeile absetzt. Gehen Sie also ehrlich mit Lecks um. Ein entfernter Aufruf kann in einen Timeout laufen oder gelingen, während die Antwort verloren geht. Eine gute entfernte Schnittstelle nimmt einen Timeout entgegen und unterscheidet eine abgelehnte Anfrage von einem unbekannten Ergebnis. Hyrums Gesetz fügt eine Warnung hinzu. Bei genügend Nutzern einer API wird irgendjemand von jedem beobachtbaren Verhalten abhängen: der Reihenfolge der Ergebnisse, dem Wortlaut eines Fehlers, sogar davon, wie lange ein Aufruf dauert. Deshalb halten stabile APIs ihre Oberfläche minimal, ändern sich durch Hinzufügen statt Abwandeln, versionieren Breaking Changes explizit und fangen Versehen mit Contract-Tests ab.

Jetzt ein ausgearbeitetes Beispiel. Ein Essenslieferdienst ruft den Code von Zahlungsanbieter A an elf Stellen direkt auf. Die Finanzabteilung möchte Anbieter B hinzunehmen und bei einem Ausfall innerhalb eines Tages wechseln können. Der Entwurf führt eine Payment-Gateway-Schnittstelle ein, die dem Shop gehört, in seinen eigenen Begriffen: autorisieren, erfassen und erstatten. Checkout und Erstattungen hängen nur von ihr ab. Jeder Anbieter erhält einen Adapter, der die Schnittstelle implementiert und in die API und die Fehlercodes dieses Anbieters übersetzt. Nur die Adapter wissen, dass es die Anbieter gibt. Die Konfiguration wählt beim Start den Adapter für jedes Land. Die verborgene Entscheidung ist, welcher Anbieter und wie er aufgerufen wird.

Die Schnittstelle verbirgt, welcher Anbieter genutzt wird, seine Version, seine Fehlercodes, seine Retry-Regeln und seine Kennungen. Ihre Vorbedingungen sind ein positiver Betrag in der kleinsten Währungseinheit, eine unterstützte Währung und ein eindeutiger Idempotenzschlüssel für jeden Versuch. Sie verspricht eines von drei Ergebnissen. Approved bedeutet, dass der Betrag reserviert ist. Declined bedeutet, dass kein Geld bewegt wurde. Unknown bedeutet, dass sich das Ergebnis nicht feststellen ließ, sodass der Aufruf mit demselben Schlüssel wiederholt werden darf. Das Netzwerkleck ist Teil des Vertrags, keine Überraschung. Der Gewinn: Einen Anbieter hinzuzufügen bedeutete früher elf Aufrufstellen und die Bestelltabelle. Jetzt sind es ein Adapter, seine Tests und ein Konfigurationseintrag.

Der vollständige Abschnitt zeigt Design by Contract im Code und acht Praktiken für stabile APIs. Information Hiding sagt Ihnen, was jedes Modul für sich behält, aber nicht, in welche Richtung Abhängigkeiten zeigen sollten. Das ist Abschnitt drei Punkt vier. Bis gleich.

Was Sie wissen müssen

Das Prinzip: verbergen, was sich wahrscheinlich ändert

In Abschnitt 3.1 zeigte David Parnas’ KWIC-Beispiel, dass eine Zerlegung eines Programms nach Verarbeitungsschritten jede Designentscheidung über mehrere Module verteilt. Seine Alternative, Information Hiding, gibt jedem Modul eine Entscheidung, die es geheim hält: wie Zeilen gespeichert werden, wie Verschiebungen berechnet werden, wann sortiert wird. Die Schnittstelle des Moduls wird so gewählt, dass sie so wenig wie möglich über diese Entscheidung verrät. Alles andere im System hängt nur von der Schnittstelle ab, sodass sich die Entscheidung ändern kann, ohne dass es jemand merkt.

Parnas’ Liste dessen, was sich zu verbergen lohnt, ist auch ein halbes Jahrhundert später noch eine gute Checkliste: Datenstrukturen und wie sie gespeichert werden; die Reihenfolge, in der Operationen ausgeführt werden; die Details von Hardware, Geräten und Betriebssystemen; Ein- und Ausgabeformate; und Algorithmen, die durch bessere ersetzt werden könnten. Moderne Ergänzungen folgen derselben Logik: welcher Drittanbieter genutzt wird, welche Datenbank, welcher Message Broker, welche Version einer externen API und welche Geschäftsregeln gerade überprüft werden.

Das Prinzip gilt auf jeder Ebene gleichermaßen. Eine Klasse verbirgt ihre Felder; ein Paket verbirgt seine internen Module; ein Service verbirgt seine Datenbank, seine Sprache und sein Deployment; die öffentliche API einer Organisation verbirgt, welche Services dahinter existieren. In jedem Fall lässt sich das Verborgene günstig ändern, und das Offengelegte wird teuer zu ändern, weil andere davon abhängen.

Kapselung ist nicht Information Hiding

Die beiden Begriffe werden oft synonym verwendet, bedeuten aber Verschiedenes. Kapselung ist ein Mechanismus: Daten werden mit den Operationen darauf gebündelt und der direkte Zugriff wird eingeschränkt, durch private Felder, Modulgrenzen oder Zugriffsmodifikatoren (Abschnitt 0.3). Information Hiding ist ein Designprinzip: Man entscheidet, welche Entscheidungen geheim bleiben, und gestaltet die Schnittstelle so, dass sie sie nicht verrät. Kapselung ist eines der Werkzeuge, mit denen Sie Informationen verbergen; was zu verbergen ist, entscheidet sie nicht für Sie.

Man kann leicht perfekt kapseln und trotzdem nichts verbergen. Eine Klasse mit privaten Feldern und je einem öffentlichen Getter und Setter für jedes davon ist vollständig gekapselt, doch jeder Aufrufer weiß genau, welche Felder es gibt und welche Typen sie haben. Ändern Sie die interne Darstellung, bricht jeder Aufrufer. Auch das Gegenteil kommt vor: Python erzwingt überhaupt keine Privatheit, und doch kann ein gut entworfenes Python-Modul seine Entscheidungen durch eine sorgfältig gewählte Menge von Funktionen sehr wirksam verbergen.

# Encapsulated, but the representation leaks through the interface
class Price:
    def __init__(self, pounds: float):
        self._pounds = pounds
    def get_pounds(self) -> float:          # callers now depend on 'float pounds'
        return self._pounds
    def set_pounds(self, value: float):
        self._pounds = value

# Information hiding: callers ask for what they need, not for the field
class Money:
    def __init__(self, minor_units: int, currency: str):
        self._minor, self._currency = minor_units, currency
    def add(self, other: "Money") -> "Money": ...
    def allocate(self, ratios: list[int]) -> list["Money"]: ...
    def format(self, locale: str) -> str: ...
# Switching from pence to a decimal type later changes only this class.
Beide Klassen sind gekapselt; nur die zweite verbirgt die Entscheidung, wie ein Preis gespeichert wird.

Abstraktion und Schnittstellen als Verträge

Eine Abstraktion ist eine vereinfachte Sicht auf etwas, die behält, was für einen Zweck wichtig ist, und den Rest weglässt. Eine Datei ist eine Abstraktion über Plattenblöcke, Caches und Treiber; eine Zahlung ist eine Abstraktion über ein Kartennetzwerk, die API eines Anbieters und den Abwicklungsprozess einer Bank. Abstraktion und Information Hiding sind zwei Seiten desselben Vorgangs: Die Wahl einer Abstraktion entscheidet, was die Schnittstelle zeigt; das Verbergen des Rests macht die Abstraktion verlässlich.

Eine Schnittstelle ist daher ein Vertrag zwischen einem Modul und seinen Nutzern. Ihr formaler Teil ist das, was ein Typprüfer oder ein API-Schema sehen kann: Operationsnamen, Parameter- und Rückgabetypen, Fehlertypen, Feldnamen in einem JSON-Body. Ihr informeller Teil ist genauso verbindlich: was die Operation tut, ihre Seiteneffekte, ob sie gefahrlos wiederholt werden kann, wie lange sie dauert, welche Fehler möglich sind und was sie bedeuten. John Ousterhout betont, dass sich der informelle Teil nur in der Dokumentation festhalten lässt und dass ein Modul keine echte Abstraktion bietet, wenn Nutzer seinen Code lesen müssen, um zu erfahren, wie man es verwendet.

Design by Contract

Design by Contract, von Bertrand Meyer in den 1980er-Jahren mit der Sprache Eiffel entwickelt und in seinem Buch Object-Oriented Software Construction sowie seinem Artikel Applying 'Design by Contract' von 1992 beschrieben, macht den Vertrag präzise. Jede Operation hat eine Vorbedingung, die der Aufrufer vor dem Aufruf erfüllen muss; eine Nachbedingung, die die Operation bei der Rückkehr garantiert, sofern die Vorbedingung galt; und jede Klasse oder jedes Modul hat Invarianten, Bedingungen, die zwischen je zwei Operationen gelten. Der Vertrag weist Verantwortung zu: Ist eine Vorbedingung verletzt, liegt der Fehler beim Aufrufer; ist eine Nachbedingung verletzt, liegt er beim Anbieter.

Meyer argumentierte gegen die entgegengesetzte Gewohnheit, die defensive Programmierung, bei der jede Routine alles prüft, falls jemand sie falsch aufruft. Redundante Prüfungen fügen Code, Komplexität und neue Fehlerpfade hinzu. Stattdessen sollte jede Bedingung klar in der Verantwortung einer Seite liegen. Eine Routine mit einer anspruchsvollen Vorbedingung ist einfacher; eine Routine mit einer toleranten ist leichter aufzurufen; die Wahl ist eine Designentscheidung, die ausdrücklich getroffen wird. An den Grenzen eines Systems, wo Eingaben von nicht vertrauenswürdigen Nutzern oder anderen Organisationen kommen, ist Validierung nicht optional: Dort besagt der Vertrag, dass das Grenzmodul selbst alles prüft (Kapitel 8).

class Account:
    """Invariant: balance_pence >= -overdraft_limit_pence."""

    def withdraw(self, amount_pence: int) -> None:
        # Preconditions (the caller's obligations)
        if amount_pence <= 0:
            raise ValueError("amount must be positive")
        if self.balance_pence - amount_pence < -self.overdraft_limit_pence:
            raise InsufficientFunds(self.id)
        old = self.balance_pence
        self.balance_pence -= amount_pence
        # Postcondition (the supplier's guarantee)
        assert self.balance_pence == old - amount_pence
        assert self.balance_pence >= -self.overdraft_limit_pence  # invariant
Ein schriftlich festgehaltener Vertrag: Vorbedingungen werden beim Eintritt geprüft, Nachbedingung und Invariante werden angegeben und per Assertion geprüft.

Dieselben drei Ideen lassen sich auf Services übertragen. Eine API-Spezifikation, etwa ein OpenAPI-Dokument, nennt die Vorbedingungen (Pflichtfelder, Formate, Authentifizierung), die Nachbedingungen (die Antwort und die Zustandsänderung, etwa es existiert eine Bestellung mit dem Status PLACED) und die Invarianten, auf die sich ein Client verlassen darf (die Gesamtsumme einer Bestellung ändert sich nach der Zahlung nie). Consumer-Driven-Contract-Tests, bei denen jeder Client die Erwartungen veröffentlicht, auf die er sich verlässt, und der Anbieter sie in seinem Build ausführt, prüfen diese Verträge automatisch.

Leaky Abstractions

Im November 2002 veröffentlichte Joel Spolsky The Law of Leaky Abstractions, das besagt: Alle nicht trivialen Abstraktionen sind bis zu einem gewissen Grad undicht. Eine Leaky Abstraction ist eine, deren verborgene Details ihre Nutzer dennoch beeinflussen. TCP bietet einen zuverlässigen Datenstrom über ein unzuverlässiges Netzwerk, bis ein Kabel durchtrennt wird und es das nicht mehr kann. SQL lässt Sie angeben, was Sie wollen, nicht wie man es bekommt, bis zwei logisch gleichwertige Abfragen sich in der Geschwindigkeit um den Faktor tausend unterscheiden und Sie den Abfrageplan lesen müssen. Ein objektrelationaler Mapper verbirgt SQL, bis eine Schleife stillschweigend eine Abfrage pro Zeile absetzt.

Die Folgerung für Architekten ist nicht, dass Abstraktionen schlecht sind, sondern dass eine Schnittstelle ehrlich mit den Details umgehen sollte, die durchsickern werden. Das klassische Beispiel ist der entfernte Aufruf, der genau wie ein lokaler Funktionsaufruf aussieht. Netzwerkaufrufe sind um Größenordnungen langsamer, können in einen Timeout laufen, können auf dem Server gelingen, während die Antwort verloren geht, und können unabhängig vom Aufrufer ausfallen; Abschnitt 6.1 führt diese als Fallacies of Distributed Computing auf. Eine gute entfernte Schnittstelle legt das offen: Sie nimmt einen Timeout entgegen, liefert Fehler, die abgelehnt von unbekanntem Ergebnis unterscheiden, und akzeptiert einen Idempotenzschlüssel, damit Wiederholungen gefahrlos sind. Das Netzwerk vollständig zu verbergen ist das Leck, das nur darauf wartet aufzutreten.

Tiefe und flache Module

John Ousterhouts A Philosophy of Software Design (Erstausgabe 2018; zweite Auflage 2021) bietet eine nützliche Art, eine Schnittstelle zu beurteilen. Stellen Sie sich jedes Modul als Rechteck vor: Die obere Kante ist seine Schnittstelle, die jeder Nutzer lernen muss, und die Fläche ist die Funktionalität, die es bietet. Ein tiefes Modul hat eine kleine Schnittstelle über viel Funktionalität und zahlt die Kosten des Erlernens daher um ein Vielfaches zurück. Ein flaches Modul hat eine Schnittstelle, die fast so komplex ist wie das, was es tut, und verursacht daher Kosten, ohne viel zu verbergen.

Sein Lieblingsbeispiel für Tiefe ist die Datei-Ein- und -Ausgabe unter Unix: eine Handvoll Aufrufe (open, read, write, seek, close) verbirgt Dateisysteme, Plattenlayout, Caching, Berechtigungen, Geräte und gleichzeitigen Zugriff. Sein Beispiel für Flachheit ist eine Klasse, deren Methoden jeweils einen Aufruf an eine andere Klasse weiterreichen, oder eine Linked-List-Klasse, deren Schnittstelle fast alles offenlegt, was sie tut. Ousterhout argumentiert, dass der verbreitete Rat, Klassen und Methoden sehr klein zu halten, zu dem führen kann, was er Classitis nennt: viele flache Klassen, deren Schnittstellen zusammen mehr Komplexität ergeben als ein einziges tieferes Modul.

Tiefe und flache Module

Ein tiefes Modul hat eine schmale Schnittstelle über großer Implementierung; ein flaches Modul eine breite Schnittstelle über wenig ImplementierungTiefes Modulkleine Schnittst.große verborgene Implementierg.Pufferung, Caching, Retries,Gerätetreiber, Berechtigungen,Plattenlayout, Parallelitätz. B. open, read, write, seek, closehoher Nutzen für den LernaufwandFlaches Modulgroße Schnittst.wenig Verhaltenz. B. eine Wrapper-Klasse,deren Methoden je einenAufruf weitergebenkostet fast so viel Lernaufwand, wie es spart
Ousterhouts Bild: Die Breite der oberen Kante ist die Schnittstelle, die Nutzer lernen müssen; die Fläche darunter ist die Funktionalität, die sie verbirgt.

Nicht alle stimmen zu, wie weit Ousterhout das treibt. Befürworter kleiner Klassen und Funktionen, etwa Robert C. Martin in Clean Code, argumentieren, dass winzige Einheiten leichter zu benennen, zu testen und wiederzuverwenden sind; Ousterhout und Martin haben die Frage in einem 2025 veröffentlichten schriftlichen Austausch diskutiert. Beide Seiten sind sich über das zugrunde liegende Ziel einig: die Komplexität zu verringern, die ein Leser im Kopf behalten muss. Auf Architekturebene ist der Tiefentest besonders wertvoll: Ein Service, dessen API seine Datenbanktabellen eins zu eins abbildet, mit Anlegen, Lesen, Ändern und Löschen für jede, ist flach und schiebt die gesamte eigentliche Logik auf seine Clients ab.

Hyrums Gesetz: Alles Beobachtbare wird zum Vertrag

Hyrums Gesetz, benannt nach Hyrum Wright, damals Softwareentwickler bei Google, durch seinen Kollegen Titus Winters und bekannt gemacht durch Software Engineering at Google (2020), stellt fest: Bei einer ausreichenden Zahl von Nutzern einer API ist es egal, was Sie im Vertrag versprechen; von allen beobachtbaren Verhaltensweisen Ihres Systems wird irgendjemand abhängen. Die Reihenfolge, in der eine Funktion Elemente aus einer ungeordneten Menge liefert, der genaue Wortlaut einer Fehlermeldung, ein Feld, das immer vorhanden ist, obwohl es als optional dokumentiert ist, die Tatsache, dass ein Aufruf 50 Millisekunden dauert: Wenn Nutzer es sehen können, werden sich einige darauf verlassen.

Das Gesetz erklärt, warum Information Hiding bewusst entworfen und nicht erhofft werden muss. Jedes Verhalten, das Sie versehentlich offenlegen, wird Teil des faktischen Vertrags, und wer es ändert, bricht etwas bei irgendjemandem, ganz gleich, wie deutlich die Dokumentation es als unspezifiziert bezeichnet hat. Manche Teams randomisieren unspezifiziertes Verhalten absichtlich, etwa die Iterationsreihenfolge, damit niemand davon abhängig werden kann; Go randomisiert aus diesem Grund die Iterationsreihenfolge von Maps. Andere halten Antwort-Payloads schlank, damit es weniger gibt, wovon man abhängen kann.

Stabile APIs zwischen Komponenten entwerfen

Zwischen Komponenten und Services sind Schnittstellen das Teuerste, was man ändern kann, weil jede Änderung mit allen Konsumenten abgestimmt werden muss. Die folgenden Praktiken ergeben sich aus allem Obigen; Abschnitt 0.6 hat APIs eingeführt, und Abschnitt 7.7 behandelt Schemakompatibilität ausführlich.

Praktiken für stabile Komponenten- und Service-APIs

PraktikWas sie bedeutetWarum sie funktioniert
Minimale OberflächeNur Operationen und Felder offenlegen, die ein bekannter Konsument braucht; mehr auf Anfrage ergänzen.Weniger, wovon man abhängen kann, also weniger, was brechen kann (Hyrums Gesetz).
Die Domäne modellieren, nicht die SpeicherungOperationen wie place_order und cancel_order anbieten statt tabellenförmigem Anlegen, Ändern und Löschen.Die Speicherung kann sich ändern, ohne dass sich der Vertrag ändert; das Modul bleibt tief.
Additive ÄnderungOptionale Felder und neue Operationen hinzufügen; bestehende innerhalb einer Version nie umbenennen, entfernen oder in ihrer Bedeutung ändern.Alte Clients funktionieren weiter; neue Clients nutzen die neuen Teile.
Tolerant ReaderClients ignorieren Felder, die sie nicht verstehen, und hängen nicht von der Feldreihenfolge ab.Anbieter können Felder hinzufügen, ohne jemanden zu brechen.
Explizite VersionierungFür Breaking Changes eine neue Hauptversion einführen (in der URL, einem Header oder dem Event-Schema) und Alt und Neu für einen veröffentlichten Zeitraum parallel betreiben.Konsumenten migrieren nach ihrem eigenen Zeitplan; Deployment-Kopplung wird vermieden.
Expand and ContractDie neue Form hinzufügen, Konsumenten migrieren, per Monitoring bestätigen, dass niemand mehr die alte Form nutzt, und sie dann entfernen, jeweils als getrennte Releases.Breaking Changes erfolgen ohne gemeinsames Release.
Contract-TestsKonsumenten veröffentlichen ihre Erwartungen; der Build des Anbieters führt sie aus.Versehentliche Breaking Changes werden vor dem Deployment erkannt.
Ehrliche FehlersemantikTimeouts, Wiederholbarkeit, Idempotenz und die Bedeutung von Fehlern als Teil des Vertrags dokumentieren.Vermeidet die schlimmsten Lecks der Netzwerkabstraktion.

Ausgearbeitetes Beispiel: eine Zahlungsschnittstelle, die den Anbieter verbirgt

Ein Essenslieferdienst wickelt Zahlungen über Anbieter A ab. Die Finanzabteilung möchte Anbieter B wegen niedrigerer Gebühren in einem Land hinzunehmen und bei einem Ausfall eines Anbieters innerhalb eines Tages wechseln können. Heute ruft der Checkout-Code das SDK von Anbieter A an elf Stellen direkt auf, nutzt dessen Fehlercodes, um zu entscheiden, was dem Kunden angezeigt wird, und speichert dessen Transaktionsobjekte in der Bestelltabelle. Jedes davon ist eine Entscheidung, die durchgesickert ist.

Der Entwurf führt eine Schnittstelle PaymentGateway ein, die dem Shop gehört, nicht einem Anbieter. Sie ist in den eigenen Begriffen des Shops formuliert: Beträge als ganze Zahlen in der kleinsten Währungseinheit mit einer Währung, die Bestellkennung des Shops, ein Idempotenzschlüssel und drei Ergebnisse. Jeder Anbieter erhält einen Adapter, der die Schnittstelle implementiert und in die API, die Fehlercodes und die Eigenheiten dieses Anbieters übersetzt und zurück. Die Konfiguration wählt den Adapter je Land. Das ist derselbe Schritt wie der Vermittler in Abschnitt 2.6, nun als Information Hiding betrachtet: Die verborgene Entscheidung ist, welcher Anbieter und wie er aufgerufen wird.

from typing import Protocol, Literal
from dataclasses import dataclass

@dataclass(frozen=True)
class PaymentResult:
    outcome: Literal["approved", "declined", "unknown"]  # unknown: safe to retry
    reference: str | None        # our reference, not the provider's object

class PaymentGateway(Protocol):
    def authorise(self, order_id: str, amount_minor: int, currency: str,
                  idempotency_key: str) -> PaymentResult: ...
    def capture(self, reference: str) -> PaymentResult: ...
    def refund(self, reference: str, amount_minor: int) -> PaymentResult: ...

class ProviderBAdapter:            # implements PaymentGateway
    def authorise(self, order_id, amount_minor, currency, idempotency_key):
        resp = self._client.create_charge(value=amount_minor / 100,   # B wants decimals
                                          currency=currency, key=idempotency_key)
        return PaymentResult(_map_status(resp.code), _store_ref(resp.id))
Die Schnittstelle gehört dem Shop und spricht seine Sprache; nur die Adapter wissen, dass es einen Anbieter gibt.

Die Zahlungsschnittstelle und ihre Adapter

Checkout hängt von einer PaymentGateway-Schnittstelle ab, die dem Shop gehört; Adapter für zwei Anbieter implementieren sie, und nur sie kennen die APIs der AnbieterCheckoutErstatt.gehört dem ShopPaymentGatewayauthorise, capture,refundKonfigurationje Land: Anbieter BProviderAAdapterProviderBAdapterAPI von AAPI von Bwählt Adapterbeim StartAufrufer sehen nur die Schnittstelle; Anbieternamen, Fehlercodes, Retries und Währungsregeln bleiben in den AdapternDurchgezogen: hängt ab von oder ruft auf. Gestrichelt: implementiert oder wählt.
Aufrufer hängen von einer Schnittstelle ab, die dem Shop gehört; Adapter implementieren sie, und die Konfiguration wählt, welcher läuft.

Ausgearbeitetes Beispiel

Was die Schnittstelle verbirgt und was sie bewusst nicht verbirgt

Den Entwurf an den Ideen dieses Abschnitts prüfen.

Verborgene EntscheidungWelcher Anbieter, sein SDK und seine API-Version, seine Fehlercodes, seine Retry-Regeln, seine Dezimal- oder Ganzzahlbeträge, seine Transaktionskennungen.
Vertrag (Vorbedingungen)Positiver Betrag in der kleinsten Währungseinheit, eine unterstützte Währung nach ISO 4217, ein eindeutiger Idempotenzschlüssel pro Zahlungsversuch.
Vertrag (Nachbedingungen)approved bedeutet, dass der Betrag reserviert ist und eine Referenz zurückgegeben wird; declined bedeutet, dass kein Geld bewegt wurde; unknown bedeutet, dass sich das Ergebnis nicht feststellen ließ und derselbe Aufruf mit demselben Schlüssel wiederholt werden darf.
Bewusstes Nicht-VerbergenDie Schnittstelle legt unknown offen, statt so zu tun, als würden Zahlungen immer gelingen oder scheitern: Das Netzwerkleck ist Teil des Vertrags, keine Überraschung.
Kosten eines neuen AnbietersVorher: Änderungen an 11 Aufrufstellen plus der Bestelltabelle. Nachher: eine neue Adapterklasse, ihre Tests und ein Konfigurationseintrag.
TiefeDrei Operationen verbergen eine beträchtliche Menge anbieterspezifischer Logik: eher tief als flach.

Achten Sie auf die Falle des kleinsten gemeinsamen Nenners: Bietet Anbieter B eine nützliche Funktion, die A fehlt, etwa Ratenzahlung, muss die Schnittstelle entweder um eine optionale Fähigkeit wachsen, oder die Funktion ist nicht verfügbar. Abstraktionen über Lieferanten tauschen immer einen Teil der Stärken jedes Lieferanten gegen die Freiheit zu wechseln.

Hyrums Gesetz gilt trotzdem: Beginnt der Checkout, von der typischen Latenz von Anbieter A abzuhängen, wird ein langsamerer Anbieter B das offenlegen. Nehmen Sie Timeouts in den Vertrag auf.

Wie Fachleute es anwenden

Erfahrene Designer beginnen eine Schnittstelle bei den Bedürfnissen ihrer Aufrufer, formuliert in deren Sprache, und entwerfen erst dann die Implementierung. Sie führen eine Liste der Entscheidungen, die jedes wichtige Modul verbirgt, und prüfen neue Schnittstellen dagegen. Sie behandeln jede öffentliche API als Produkt mit einem Lebenszyklus (Vorschlag, Review, Versionierung, Abkündigung), weil nach Hyrums Gesetz alles Veröffentlichte genutzt wird. Und sie akzeptieren, dass Abstraktionen undicht sind, und machen die Lecks deshalb explizit: Latenzbudgets, Fehlersemantik und Retry-Regeln stehen im Vertrag.

Information Hiding sagt Ihnen, was jedes Modul für sich behalten sollte. Es sagt Ihnen nicht, in welche Richtung die Abhängigkeiten zwischen Modulen zeigen sollten oder was passiert, wenn sie Schleifen bilden. Das ist das Thema von Abschnitt 3.4.

Prüfen Sie Ihr Verständnis

Warum kann eine Klasse mit privaten Feldern und öffentlichen Gettern und Settern für alle davon beim Information Hiding trotzdem versagen?

Weil die Schnittstelle die genaue interne Darstellung preisgibt, sodass Aufrufer davon abhängen und jede Änderung der Darstellung sie bricht; Kapselung ist vorhanden, aber keine Entscheidung ist verborgen.

Wer ist bei Design by Contract schuld, wenn eine Vorbedingung verletzt wird?

Der Aufrufer, denn die Vorbedingung zu erfüllen ist seine Pflicht; eine verletzte Nachbedingung weist auf einen Fehler beim Anbieter hin.

Was macht ein Modul im Sinne Ousterhouts tief?

Es bietet viel Funktionalität hinter einer kleinen, einfachen Schnittstelle, sodass die Kosten, die Schnittstelle zu lernen, im Vergleich zu dem, was sie verbirgt, gering sind.

Warum spricht Hyrums Gesetz für kleine API-Antworten?

Auf jedes beobachtbare Feld oder Verhalten wird sich irgendwann ein Client verlassen, sodass alles Offengelegte schwer zu ändern wird; weniger offenzulegen lässt mehr Freiheit.

Weiter · 3.4AbhängigkeitenIn welche Richtung Abhängigkeiten zeigen sollten und warum: Robert C. Martins Prinzipien der azyklischen Abhängigkeiten, der stabilen Abhängigkeiten und der stabilen Abstraktionen, die Metriken Instabilität, Abstraktheit und Distanz, Zyklen auflösen, Schichtenbildung und automatisierte Abhängigkeitsprüfungen.Weiter →

Quellen und weiterführende Literatur

Haben Sie eine Frage zu diesem Abschnitt? ArchitectBot, der KI-Lernassistent des Kurses, antwortet zuerst aus dem Kurs.

Fragen: „Wie sollte ich eine interne API zwischen den Services zweier Teams entwerfen, damit wir die Implementierung ändern können, ohne uns gegenseitig etwas kaputtzumachen?“