Ein Team baut ein System, das Anfragen in drei Prioritäten einordnet: urgent, normal, low.
Drei Werte, klar begrenzt.
Die erlaubten Werte landen direkt im Programmcode. Monate später kommt eine vierte Priorität hinzu. Aus einer Änderung des Vokabulars werden eine Code-Änderung, ein Review, neue Tests und ein Deployment.
Dabei hat das Team mit dem festen Wertebereich zunächst nichts falsch gemacht.
Der Fehler liegt an einer anderen Stelle: Festgelegt wurde mit einprogrammiert verwechselt.
Ein geschlossener Wertebereich darf geschlossen sein
Nicht jede Schnittstelle muss beliebige Werte akzeptieren.
Eine Zelle - im Sinne des Cell Models eine eigenständige Einheit mit einer klaren Verantwortung - darf sehr genau beschreiben, welche Nachrichten sie versteht. Das Cell Model sieht ausdrücklich vor, dass eine Zelle ihre eingehenden Ereignisse beschreibt und nicht jede Nachricht verarbeitet.
Ein Vertrag kann deshalb durchaus verlangen:
priority muss ein Wert aus dem Vokabular priorities sein
Das ist ein geschlossener Vertrag. Ein Wert außerhalb dieser Menge ist ungültig.
Daraus folgt aber noch nicht:
if priority == "urgent"
or priority == "normal"
or priority == "low" then
Der Vertrag bestimmt die Grenze. Der Programmcode muss deren aktuellen Inhalt nicht selbst besitzen.
Vertrag und Vokabular sind zwei verschiedene Dinge
Genau an dieser Stelle verschwimmen in Programmen häufig zwei Ebenen.
Der Vertrag beschreibt beispielsweise:
priority:
type: vocabulary
vocabulary: priorities
required: true
Das Vokabular beschreibt dagegen:
priorities:
- urgent
- normal
- low
Der erste Teil sagt, welche Art von Wert erlaubt ist. Der zweite sagt, welche Werte derzeit zu dieser Menge gehören.
Der Code kann damit weiterhin streng prüfen: Ist der empfangene Wert Bestandteil von priorities? Er muss dafür aber nicht wissen, dass urgent, normal und low existieren - und er darf es nicht wissen, wenn sich diese Menge zur Laufzeit verändern können soll. Sobald urgent als Bedingung im Code steht, ist nicht nur eine Regel implementiert, sondern Weltwissen in die Implementierung eingebrannt. Dieses Wissen kann die Zelle anschließend nicht mehr selbst verändern.
Vier Ebenen
Bei unserer Arbeit taucht diese Trennung inzwischen an mehreren Stellen auf. Hilfreich sind dabei vier unterschiedliche Ebenen.
Eine Fähigkeit beschreibt, was eine Einheit grundsätzlich leisten kann - etwa Anfragen nach Priorität zu behandeln. Damit ist noch nicht festgelegt, wie eine Anfrage technisch aussieht.
Der Vertrag beschreibt die gemeinsame Sprache: Eine Anfrage besitzt eine Priorität, und die Priorität muss aus dem vereinbarten Vokabular stammen. Damit ist die Grenze eindeutig.
Im Vokabular stehen die momentan vereinbarten Werte - urgent, normal, low. Diese Menge kann geschlossen sein. Sie muss trotzdem nicht im Programmcode stehen.
Zur Laufzeit wird schließlich ein konkreter Wert übertragen:
priority: urgent
Der Empfänger muss jetzt nicht für urgent programmiert worden sein. Er muss wissen, wie er einen Wert gegen das vereinbarte Vokabular prüft und was eine Priorität innerhalb seiner Verantwortung bedeutet.
Damit bleiben vier Fragen getrennt: Was kann ich? Wie reden wir darüber? Welche Begriffe gelten gerade? Was wurde konkret gesagt?
Lernen verlangt veränderbare Inhalte
Die Trennung wird besonders wichtig, wenn ein Programm sein Verhalten während der Laufzeit verändern können soll.
Ein Programm kann nur das verändern, was nicht bereits Bestandteil seines unveränderlichen Programmcodes ist. Stehen urgent, normal und low in einem switch, kann das System zwar neue Daten empfangen. Eine neue Priorität kann es aber nicht wirklich lernen - für emergency fehlt ihm eine einprogrammierte Reaktion.
Die naheliegende Lösung wäre, nicht nur die Namen der Prioritäten auszulagern, sondern auch die Eigenschaften, anhand derer die Zelle mit ihnen arbeitet:
priorities:
urgent:
order: 10
strategy: immediate
normal:
order: 20
strategy: queued
low:
order: 30
strategy: background
Der Code kennt jetzt nicht mehr urgent. Er kennt Regeln wie Reihenfolge, Reaktionsstrategie oder Bedingungen. Das Vokabular liefert die aktuellen Ausprägungen dieser Regeln. Damit verschiebt sich Wissen aus dem Programm in einen Bereich, den die Zelle oder ihre Umgebung verändern kann - und emergency lässt sich ergänzen, ohne den Code anzufassen.
Ist strategy dabei wiederum eine einprogrammierte Liste aus immediate, queued, background, ist das Problem nur eine Ebene verschoben.
Das bedeutet nicht, dass das System keine festen Regeln mehr besitzt. Im Gegenteil: Die Regeln können sehr streng festlegen, welche Eigenschaften ein Eintrag besitzen muss, welche Kombinationen erlaubt sind und welche Grenzen gelten. Der Vertrag kann dabei streng festlegen, in welcher Sprache Veränderungen derzeit beschrieben werden. Veränderbar bleibt, was mit dieser Sprache ausgedrückt wird - und im Prinzip auch die Sprache selbst.
Warum uns das gerade wieder begegnet
Solche Listen entstehen beim Programmieren schnell. Ein paar erlaubte Ereignistypen. Ein paar Statuswerte. Ein paar Operationen. Ein paar Rollen.
Der einfachste Weg ist ein enum, ein switch oder eine Tabelle direkt im Modul. Solange sich nichts verändert, sieht das sogar nach einer besonders sauberen Lösung aus: Die möglichen Werte sind an einer Stelle gesammelt und der Code kann sie eindeutig prüfen.
Erst beim nächsten Schritt zeigt sich, welche Entscheidung dabei unbemerkt gefallen ist. Nicht nur: Diese Menge ist geschlossen. Sondern zusätzlich: Dieses Programm besitzt diese Menge.
Das sind zwei unterschiedliche Aussagen.
Ein Vertrag kann streng sein, ohne starr zu werden
Ein geschlossenes Vokabular ist deshalb nicht das Problem. Im Gegenteil: Ein klarer Wertebereich macht Kommunikation überprüfbar. Sender und Empfänger wissen, welche Begriffe Teil ihrer gemeinsamen Sprache sind.
Die eigentliche Frage lautet, wer dieses Wissen besitzen muss. Liegt es im Programmcode, verändert jede Änderung des Vokabulars auch das Programm. Liegt es in Vertragsdaten oder einer Konfiguration, kann der Code unverändert bleiben, solange sich seine Fähigkeit und die Form des Vertrags nicht ändern.
Damit geht es vor dem nächsten switch um mehr als ein gespartes Deployment: Wie viel muss ein Programm wissen, damit es handeln kann - und wie wenig darf es wissen, damit es noch lernen kann?