Die meisten Bugs verstecken sich in der Lücke zwischen „Was es tun sollte” und „Was es tatsächlich tut”

Du schreibst zuerst den Code, dann die Tests, und stellst fest, dass der Code falsch war. Das ist der Standard-Loop. Deshalb frisst Debugging die Hälfte der meisten Projekt-Timelines.

Box Structures kehren das um. Du definierst das Verhalten, bevor du es schreibst, verifizierst diese Definition mathematisch und übersetzt sie Schicht für Schicht in Code. Das Ergebnis ist ein Modul, das konstruktionsbedingt korrekt ist – nicht korrekt, weil die Tests zufällig durchlaufen.

Das klingt unmöglich, bis man sieht, wie klein jede Schicht ist.

Was ist eine Box Structure?

Eine Box Structure beschreibt ein Software-Modul auf drei Abstraktionsebenen, jede eine strikte Verfeinerung der vorherigen:

  • Black Box: Welcher Stimulus erzeugt welche Response? Kein State. Keine Implementierung. Nur eine pure Function von History zu Output.
  • State Box: Welchen State merkt sich das Modul, und wie transformiert jeder Stimulus diesen State und erzeugt eine Response?
  • Clear Box: Der tatsächliche Code.

Du designst von außen nach innen. Die Black Box ist der Contract. Die State Box ist das Datenmodell. Die Clear Box ist der Code. In jedem Schritt beweist du, dass die untere Schicht die obere erfüllt, bevor du weitergehst.

Das ist keine nachträgliche Dokumentation. Black Box und State Box sind formale Spezifikationen. Sie sind das Design. Der Code kommt zuletzt.

Die Black Box: History bestimmt alles

Eine Black Box definiert ein Modul durch seine Stimulus-History und seine Response. Die Response auf einen Stimulus hängt von jedem vorherigen Stimulus ab.

Betrachte einen einfachen Token Bucket Rate Limiter. Die Black Box Specification sagt:

StimulusCondition on HistoryResponse
request(n)tokens available >= ngrant(n)
request(n)tokens available < ndeny
add_tokens(k)any historyno response, update history

Die Black Box sagt nicht, wie Tokens getrackt werden. Sie sagt: Gegeben diese History von Stimuli, das ist die geforderte Response.

Du kannst das verifizieren, bevor irgendein Code existiert. Schreibe Sequenzen von Stimuli, berechne die erwarteten Responses per Hand und prüfe, ob die Specification korrekt arbeitet. Kein Compiler. Kein Test Runner. Nur Logik.

Die State Box: Memory hinzufügen, ohne Annahmen hinzuzufügen

Die State Box verfeinert die Black Box durch eine State Variable, die die History implizit macht. Statt die volle Stimulus-History mitzuschleppen, merkt sich das Modul eine komprimierte Repräsentation.

Für den Rate Limiter führt die State Box tokens, die aktuelle Anzahl verfügbarer Tokens, ein. Die Specification wird:

StimulusPrecondition on StateResponseNew State
request(n)tokens >= ngrant(n)tokens - n
request(n)tokens < ndenytokens (unchanged)
add_tokens(k)anyno responsetokens + k

Der kritische Schritt ist der Beweis, dass diese State Box äquivalent zur Black Box ist. Die State Variable tokens muss die relevante History korrekt repräsentieren. Wenn die Anwendung der Black Box-Regeln dieselben Responses produziert wie der State Box Counter, ist die Verfeinerung gültig.

Dieser Beweis ist meist kurz. Du verifizierst eine einseitige Spezifikation, keine tausendzeilige Codebase.

Die Clear Box: Code, der dich nicht überraschen kann

Die Clear Box ist die Implementierung. Sie wird in einer strukturierten Sprache ohne Gotos und ohne versteckten Control Flow geschrieben. Jede Clear Box besteht aus Sequence, Alternation (if-then-else) und Iteration (while, for).

Hier ist eine Clear Box Implementation in Python:

class TokenBucket:
    def __init__(self, capacity: int, refill_rate: float):
        self.capacity = capacity
        self.tokens = capacity
        self.refill_rate = refill_rate
        self.last_refill = time.monotonic()

    def _refill(self) -> None:
        now = time.monotonic()
        elapsed = now - self.last_refill
        added = int(elapsed * self.refill_rate)
        if added > 0:
            self.tokens = min(self.capacity, self.tokens + added)
            self.last_refill = now

    def request(self, n: int) -> str:
        self._refill()
        if self.tokens >= n:
            self.tokens -= n
            return f"grant({n})"
        return "deny"

    def add_tokens(self, k: int) -> None:
        self.tokens = min(self.capacity, self.tokens + k)

Beachte, dass _refill nicht in der State Box war. Zeit existiert nicht in der Black Box oder State Box. Diese Modelle nehmen an, dass Tokens wie durch Magie erscheinen, wenn add_tokens aufgerufen wird. Die Clear Box muss diese Lücke schließen.

Hier leben die meisten Bugs. Die State Box sagt, tokens steigt um k. Die Clear Box muss auch Continuous Refilling und das Capacity Cap handhaben und dabei trotzdem zur State Box konform sein.

Wie Cleanroom Korrektheit beweist, ohne Tests laufen zu lassen

In Cleanroom schreibst du keine Unit Tests für die Clear Box. Du führst eine Verification durch.

Für jede Control Structure schreibst du ein Predicate, das vor und nach der Ausführung gelten muss. Für eine Sequence S1; S2 beweist du, dass die Postcondition von S1 die Precondition von S2 impliziert. Für ein if-then-else beweist du, dass beide Branches die Gesamt-Postcondition erfüllen. Für eine while-Schleife findest du ein Invariant und beweist, dass es beim Eintritt gilt, von jeder Iteration erhalten bleibt und die gewünschte Postcondition impliziert, wenn die Schleife terminiert.

Das ist kein Proof Assistant. Es ist Stift und Papier. Wenn du jemals informell argumentiert hast, dass eine rekursive Function terminiert, hast du bereits 90 Prozent eines Cleanroom-Proofs gemacht.

Warum die meisten Teams das nicht nutzen

Der offensichtliche Einwand ist Zeit. Eine Black Box, eine State Box und ein Hand-Proof zu schreiben klingt langsamer als einfach Code zu schreiben und Bugs zu fixen.

Harlan Mills, der Cleanroom bei IBM entwickelt hat, maß das Gegenteil. Cleanroom-Teams lieferten Code mit einer Größenordnung weniger Defekte als Kontroll-Teams, und ihre Gesamt-Entwicklungszeit war kürzer, weil sie fast keine Zeit im Debugger verbrachten.

Der weniger offensichtliche Einwand ist kulturell. Box Structures zwingen dich, vor dem Tippen nachzudenken. Die meisten Entwickler empfinden das als unangenehm. Die Spezifikation fühlt sich wie Bürokratie an. Ist sie nicht. Sie ist das Design. In Cleanroom ist das Design in einer Notation geschrieben, die präzise genug zum Verifizieren ist – nicht in einem Diagramm, das der erste Implementierungs-Entwurf ignoriert.

Wann Box Structures den Overhead wert sind

Du musst nicht jede Utility Function auf diese Weise spezifizieren. Box Structures brillieren dort, wo ein Bug mehr kostet als die Zeit zum Schreiben einer Spezifikation: Payment Processing, Authorization, Distributed Consensus, protocols und Workflow Engines. Sie helfen auch Teams, die immer wieder dieselbe Kategorie von Bugs in Produktion bringen und sie nie in Tests erwischen.

Ein leichter Weg anzufangen

Du musst nicht den gesamten Cleanroom-Prozess adoptieren. Borg dir die Box Structure-Idee für ein einzelnes Modul.

Wähle eine Function, die Produktions-Issues verursacht hat. Schreibe ihre Black Box: eine Tabelle von Inputs, Conditions und Expected Outputs. Sieh nicht auf den bestehenden Code. Schreibe, was sie tun sollte, nicht was sie tut.

Dann schreibe die State Box. Welchen State braucht sie? Wie transformiert jeder Input diesen State? Vergleiche sie mit deiner Implementierung. Wo sie sich unterscheiden, hast du einen Bug oder eine undocumented Assumption gefunden.

Hier ist ein minimales Template in Python, das du adaptieren kannst:

"""
Black Box Specification: Rate Limiter

Stimuli: request(n), add_tokens(k)
History: sequence of all stimuli received

Rules:
  1. After any sequence of stimuli, tokens available =
     sum(add_tokens.k) - sum(grant.n)
  2. request(n) grants iff tokens available >= n
  3. tokens available never exceeds capacity

State Box Refinement:
  State: tokens (integer, 0 <= tokens <= capacity)
  Invariant: tokens accurately represents available tokens
"""

class TokenBucket:
    """Clear box implementation of the rate limiter state box."""

    def __init__(self, capacity: int):
        self.capacity = capacity
        self.tokens = capacity

    def request(self, n: int) -> str:
        if n <= 0:
            raise ValueError("request must be positive")
        if self.tokens >= n:
            self.tokens -= n
            return f"grant({n})"
        return "deny"

    def add_tokens(self, k: int) -> None:
        if k < 0:
            raise ValueError("cannot add negative tokens")
        self.tokens = min(self.capacity, self.tokens + k)

Die Comments sind die Spezifikation. Der Code ist die Implementierung. Sie im selben File zu halten, macht die Verfeinerung sichtbar und reviewbar.

Der echte Wert ist die Separation of Concerns

Box Structures sind keine Magie. Sie werden nicht jeden Bug fangen. Was sie tun, ist eine Disziplin zu erzwingen, die die meisten Entwicklungsprozesse überspringen: zu definieren, was korrekt bedeutet, bevor du es baust.

Die Black Box trennt den Contract des Moduls von seinen Interna. Die State Box trennt das Datenmodell vom Code. Die Clear Box trennt die Implementierung vom Proof. Jede Schicht hat einen Job, und du verifizierst ihn, bevor du zur nächsten gehst.

Das ist der Grund, warum es dir etwas angehen sollte. Die Lücke zwischen „was es tun sollte” und „was es tut” ist dort, wo deine Bugs leben, und Box Structures sind eine systematische Methode, diese Lücke zu schließen, bevor du einen einzigen Test schreibst.

Wenn du tiefer einsteigen willst: Mills’ Cleanroom Software Engineering und die originalen IBM Technical Reports sind immer noch die klarsten Referenzen. Die Ideen sind alt. Die Bugs, die sie verhindern, nicht.