Der Bibliotheksvertrag
Eine Anwendung wird von ihren Benutzern beurteilt; eine Bibliothek von Programmierern, und zwar in dem bestimmten Moment – wenn sie etwas wollen, das der Autor nicht vorhergesehen hat. Eine gute Bibliothek ist nicht die, die bequem ist, wenn sie wie beabsichtigt verwendet wird. Es ist die, die nicht weggeworfen werden muss, wenn sie anders verwendet wird.
Jede der folgenden Regeln folgt aus einer Asymmetrie. Innerhalb der Bibliothek darfst du alles ändern, für immer. Außerhalb ist alles, was ein Aufrufer beobachten kann, eingefroren, sobald jemand zum ersten Mal davon abhängt – und bei genügend Aufrufern hängt jemand von allem ab.
Was einfriert und wann
Hyrums Gesetz ist die Betriebsbedingung, keine Kuriosität: Bei genügend Nutzern wird jedes beobachtbare Verhalten tragend – Ausnahmeformulierungen, Aufzählungsreihenfolge, Protokollausgabe, Zeitverhalten, sogar Verhalten, das Sie als undefiniert dokumentiert haben. „Das war nie im Vertrag“ repariert den Aufrufer nicht, der dagegen ausgeliefert hat.
Die Einfrierlinie liegt also bei der Beobachtbarkeit, nicht beim public-Schlüsselwort,
und drei Konsequenzen ergeben sich:
- Die Oberfläche beginnt klein. Ein Symbol, einmal öffentlich, ist ein Versprechen, das man erweitern, aber nie leise zurückziehen kann. „Öffentlich für jetzt“ ist Schulden ohne Nutzen; nur bei Bedarf freigeben.
- Formate überleben Code. Ein Dateiformat, ein Netzwerkprotokoll, ein persistiertes Schema überlebt jede Neuschreibung der Bibliothek darum herum. Die interne Struktur so zu serialisieren, schweißt die eingefrorene Schicht an die bearbeitbare.
- Ein In-App-Modul überschreitet diese Grenze bei seinem zweiten Konsumenten, nicht beim Verpacken. Sobald zwei Aufrufer existieren, hat seine API die gleiche Einfrierökonomie wie ein veröffentlichtes Paket – nur der Sprengradius ist kleiner.
Semantische Versionierung ist die öffentliche Schreibweise derselben Grenze, und sie beginnt damit, sich zu weigern, ohne eine zu arbeiten: „Software, die semantische Versionierung verwendet, MUSS eine öffentliche API deklarieren“ (SemVer 2.0.0, Klausel 1). Sobald deklariert, werden die Erhöhungen entschieden und nicht gefühlt – eine abwärtsinkompatible Änderung an dieser API MUSS MAJOR erhöhen (Klausel 8), neue abwärtskompatible Funktionalität MUSS MINOR erhöhen (Klausel 7), eine abwärtskompatible Fehlerbehebung MUSS PATCH erhöhen (Klausel 6). Hyrums Gesetz ist das, was die Deklaration schwierig macht: Die Spezifikation friert ein, was du deklariert hast, und die Aufrufer frieren ein, was sie beobachtet haben.
Die Spezifikation hat auch einen Namen für das Modul, das die Grenze noch nicht überschritten hat. „Hauptversion null (0.y.z) ist für die anfängliche Entwicklung. ALLES kann sich jederzeit ändern“ (Klausel 4). Das ist das ehrliche Etikett für ein Modul mit einem Konsumenten, und der Tag, an dem es einen zweiten bekommt, ist der Tag, an dem 1.0.0 fällig ist, nicht der Tag, an dem es verpackt wird.
Keine Kontrolle übernehmen
Trenne Mechanismus von Richtlinie. Eine HTTP-Anfrage durchzuführen ist Mechanismus; sie dreimal mit exponentiellem Backoff zu wiederholen ist Richtlinie, und Richtlinie, die in den Kern eingebrannt ist, schränkt die Bibliothek auf Aufrufer ein, die sie teilen. Liefere bequeme Standardwerte für Protokollierung, Threading, Wiederholungen, Timeouts, Caching, Serialisierung – und mache jeden davon ersetzbar.
Die Anwendungsrolle ist einzigartig, und es ist nicht deine. Den Prozess zu beenden, auf stdout zu schreiben, SIGINT abzufangen, top-level Ausnahmehandler zu installieren, den Thread-Pool zu skalieren: genau eine Komponente pro Prozess darf dies tun, und eine Bibliothek, die es tut, bekämpft die dazu berechtigte Komponente. Das Lesen der Prozesseingaben gehört zu derselben Rolle: Umgebungsvariablen, Konfigurationsdateien und Befehlszeilenargumente werden an der Anwendungswurzel gelesen, die der Bibliothek jeden Wert als Parameter oder Optionsobjekt übergibt. Der Aufrufer wählt dann, woher ein Wert kommt, und ein Test übergibt ihn, anstatt die Maschine einzurichten.
Magie – automatische Erkennung, automatische Konvertierung, automatische Wiederverbindung, Auto-Cache – ist nur zulässig, solange der Aufrufer rekonstruieren kann, warum es getan hat, was es getan hat. Bequemlichkeit, die der Benutzer nicht debuggen kann, ist übernommene Kontrolle, keine gesparte Arbeit.
Keine versteckten Kosten hinzufügen
Wann Dinge passieren, gehört dem Aufrufer. Ein Import, der einen Thread startet, ein Konstruktor, der eine Verbindung öffnet, ein statischer Initialisierer, der Umgebungsvariablen liest – jeder verschiebt echte Arbeit zu einem Zeitpunkt, den der Aufrufer nie gewählt hat und nicht zurückverschieben kann.
Eine Abhängigkeit ist Teil deiner API. Ihre Laufzeit, ihr Update-Rhythmus, ihre ABI-Stabilität, ihre Lizenz, ihre eigenen Initialisierungsnebenwirkungen übertragen sich auf jeden Aufrufer; du wählst in ihrem Namen.
Leistungsmerkmale sind Vertrag. Eine O(1)-Suche in O(n) zu verwandeln – oder Allokationen und Kopien zu verdoppeln – ist eine bahnbrechende Änderung, die eine kompatible Signatur trägt, und Aufrufer entdecken sie in der Produktion.
Wo Eingaben schneller ankommen, als sie abgehen – Streams, Ereignisse, Protokolle, Warteschlangen – ist ein unbegrenzter interner Puffer ein verzögerter Speicherverlust. Gegendruck ist Teil des Designs, kein operationelles Detail.
Der Maßstab für die Abstraktion selbst ist Nullkosten, in beiden Klauseln von Stroustrup. Was der Aufrufer nicht nutzt, bezahlt er nicht: die Komfortebene darf die primitive Ebene nicht mit ihren Allokationen, ihrem Dispatch oder ihrer Einrichtung besteuern. Und was sie nutzen, könnten sie vernünftigerweise nicht besser von Hand schreiben: eine Abstraktion, die von handgeschriebenem Code geschlagen wird, wird umgangen, und eine umgangene Bibliothek ist eine weggeworfene mit einer Versionsnummer.
Keinen versteckten Zustand halten
Singletons, statische Caches, globale Ereignisschleifen, prozessweite Konfiguration: jede funktioniert, solange deine die einzige Bibliothek im Prozess ist, und kollidiert, sobald eine zweite Bibliothek, ein Plugin-Host, oder eine parallele Testsuite sie teilt. Zustand lebt in einer Instanz, die der Aufrufer erstellt, besitzt, und zweimal erstellen kann.
Keinen Fehler verbergen
Die API-Form erklärt, was passieren kann und wer verantwortlich ist. Fehler, Abbruch, Timeout, Teilerfolg, Abwesenheit – in den Typen modelliert, nicht über „das passiert normalerweise nie“ und eine Ausnahme geleitet. Dieselbe Form trägt die Besitzantworten, die ein Kommentar nicht erzwingen kann: wer schließt den Stream, ob der Puffer wiederverwendet werden darf, auf welchem Thread der Callback ankommt.
Behalte die Ursache.
Das Zusammenfallen eines Fehlers in false oder null entfernt genau die Informationen, die die darüberliegende Schicht benötigte, um zu entscheiden;
selbst zu protokollieren und fortzufahren, verliert dieselben Informationen und nimmt die Protokollierungsrichtlinie des Aufrufers mit.
Qualität trennt sich auf dem abnormalen Pfad. Auf dem Happy Path sind Bibliotheken austauschbar; Timeout, Wiederverbindung, Unterbrechung während des Streams, teilweises Lesen, beschädigte Daten und Wiederholung sind die Unterschiede, also entwerfe zuerst für ihre Komposition.
Abbruch lässt sich schlecht nachrüsten – ein Abbruchtoken fädelt sich durch jede Signatur, die es berührt. I/O und langlaufende Operationen akzeptieren es ab der ersten Version.
Keinen Ausweg blockieren
Baue zwei Stockwerke.
Eine High-Level-API, die den gemeinsamen Fall in einer Zeile bedient,
basierend auf öffentlichen Primitiven – ReadAsync() über ReadAsync(Memory<byte>) – damit der Aufrufer, dessen Anforderung außerhalb des oberen Stockwerks liegt, hinabsteigt, anstatt zu gehen.
Eine Bibliothek mit nur dem oberen Stockwerk wird bei der ersten Anforderung weggeworfen, die sie nicht vorhergesehen hat.
Beobachtbarkeit wird injiziert, nicht besessen: Logger, Metriken, Traces stecken in den Stack des Aufrufers. Ein Diagnoseformat, das nur deine Bibliothek liest, ist als Geisel genommene Telemetrie.
Eine Steuer, zwei Währungen
Diese Skill und Clean Architecture sind dieselbe Ablehnung in zwei Währungen. Zero-Cost-Abstraktion verweigert die Steuer auf die Laufzeitleistung; die Dependency Rule verweigert die Steuer auf die Änderbarkeit. In beiden wird eine Abstraktion danach beurteilt, was sie die abhängige Seite aufgeben lässt.
| Pflicht hier | Ihr architektonischer Name |
|---|---|
| Mechanismus getrennt von Richtlinie | Anwendungsfall getrennt von Framework und Treiber |
| Standardwerte ersetzbar | Dependency Inversion – die stabile Seite wählt die Implementierung |
| Primitiven unter der Komfortebene | Äußere Details austauschbar |
| Kein globaler Zustand | Abhängigkeiten explizit, sodass eine Grenze gezogen werden kann |
| Eine Abhängigkeit ist Teil deiner API | Die Dependency Rule |
Die gesunde Form des Aufrufers platziert deine Bibliothek unten: ein ersetzbares Detail unter ihrer Richtlinie und Domäne. Die umgekehrte Form – die Anwendung lebt innerhalb eines Frameworks, schleppt seinen Lebenszyklus, seine Typen, sein DI, sein Fehlermodell, sein asynchrones Modell – ist die Bibliothek, die als Anwendung fungiert, geschrieben im Architekturmaßstab.
Die allgemeine Form: Gute Architektur minimiert nicht die Anzahl der Abhängigkeiten, sondern die Freiheit, die jede Abhängigkeit wegnimmt.
Der Satz
Eine Abstraktion in einer Bibliothek existiert, um die Wahlmöglichkeiten des Aufrufers zu erweitern. In dem Moment, in dem sie Leistung, Kontrolle oder Informationen im Austausch nimmt, hat sie aufgehört zu abstrahieren und begonnen zu entscheiden.
Wann nicht anwenden
Ein Modul mit einem Konsumenten, einem Repository, einem Team hat die Einfrierlinie nicht überschritten – es ist bei 0.y.z in SemVer-Begriffen, ob es nun eine Nummer trägt oder nicht – und für dieses Modul jeden ersetzbaren Richtlinien-Seam zu bauen, ist Spekulation mit einem Wartungsaufwand. Behalte die Disziplinen, die von Tag eins an nichts kosten – kein globaler Zustand, Fehler, die ihre Ursache behalten, keine Arbeit, bevor der Aufrufer fragt – und füge die Seams hinzu, wenn der zweite Konsument eintrifft, was der Moment ist, in dem sie aufhören, spekulativ zu sein.
Quellen
- Semantic Versioning 2.0.0, semver.org: Klausel 1 (öffentliche API MUSS deklariert werden), Klauseln 6–8 (PATCH, MINOR, MAJOR-Erhöhungen), Klausel 4 (0.y.z), und der FAQ-Eintrag zur Veraltung (dokumentieren, dann ein Minor-Release mit der Veraltung herausgeben). Wortlaut gegen die Seite geprüft.
- Hyrums Gesetz ist eine Beobachtung, kein Standard, und wird als solche zitiert.