Du erkennst eine Schnittstelle und ihre vollständige Vertragsoberfläche — nicht nur den Methodennamen. Du führst Refactorings durch, ohne tote Aufrufstellen zurückzulassen. Und du baust Robustheit an den Grenzen deines Systems ein, nicht im Inneren.
Konkret nach Tier 3: Du hast eine eigene Refactor-Sweep-Checkliste, du verstehst, warum Typannotationen Verträge sind, und du setzt Boundary-Coercion bewusst dort ein, wo eine fremde Eingabe deinem System Schaden zufügen könnte.
In einer iMenu-Refactoring-Session wurden drei Schnittstellen geändert. Jede ließ tote Aufrufstellen zurück. Drei verschiedene Ausprägungen einer Disziplin.
# Vorher
class SessionBridge:
def __init__(self, config: BridgeConfig): ...
# Nachher (vereinfacht)
class SessionBridge:
def __init__(self): ...
In cli.py blieben fünf Click-Subkommandos zurück, die noch SessionBridge(config) aufriefen. Beim ersten Aufruf von i menu jump schlug das CLI mit TypeError: __init__() takes 1 positional argument but 2 were given fehl.
# Vorher
@dataclass
class Project:
type: str # "library", "service", ...
# Nachher
@dataclass
class Project:
project_type: str
Die Search-Funktion griff weiter auf project.type zu — AttributeError, sobald jemand suchte.
description durfte plötzlich ein Dict seinEine metadata.yaml-Datei änderte sich:
# Vorher
description: "A small CLI tool for X"
# Nachher
description:
en: "A small CLI tool for X"
de: "Ein kleines CLI-Werkzeug für X"
Die Datenklasse versprach description: Optional[str]. Die echte Eingabe war jetzt manchmal dict. Downstream rief eine Filter-Funktion description.lower() auf — AttributeError.
Drei verschiedene Bugs, eine Disziplin: Verträge ändern bedeutet, jede Konsumstelle mitzuziehen — und für unkontrollierbare Eingaben Grenzen zu setzen.
Eine Schnittstelle ist mehr als „der Methodenname". Sie ist die ganze Vertragsoberfläche, die deine Aufrufer sehen:
| Element | Beispiel |
|---|---|
| Konstruktor-Signatur | SessionBridge(config: BridgeConfig) |
| Methoden-Signaturen | def jump(name: str) -> bool |
| Rückgabe-Typ und -Form | Optional[str], list[Project] |
| Öffentliche Attribute | project.project_type, bridge.session_id |
| Typannotationen | sind ein Vertrag, keine Höflichkeit |
| Ausnahme-Verträge | „wirft KeyError, wenn …" |
Wenn sich auch nur eines davon ändert, wird jede Aufrufstelle zur Hausaufgabe. Die Sprache verlangt vom Aufrufer, dass er sich anpasst — du musst die Aufrufer finden, sonst tut die Laufzeit es für dich, später, vor einem Anwender.
bridge.jump(name)app.set_interval(2.0, self._render) — _render ist hier eine Aufrufstelle, auch ohne ()test_jump() ruft bridge.jump()tools/-Skript, das das Modul nutztFaustregel: Wenn ich es umbenennen kann, ohne dass irgendwer es bemerkt, war es nie öffentlich.
Wir refactorieren bewusst, mit Disziplin. Diese Schritte sind in dieser Reihenfolge wichtig.
Schreib auf, bevor du eine Zeile änderst, was sich an der Schnittstelle ändert:
GEÄNDERT WIRD:
- Konstruktor: SessionBridge.__init__: (config) → ()
- Methodenname: rename_session() → rename()
- Rückgabe-Typ: list[str] → list[Session]
- Neue Annotation: name: str → name: str | None
- Attribut: Project.type → Project.project_type
Wenn du das nicht aufschreiben kannst, weißt du noch nicht, was du tust — und dein zukünftiges Ich hat keine Chance, die Aufrufstellen vollständig zu finden.
Pro Element der Änderungsliste:
# Methoden-Umbenennung — \b stellt sicher, dass _render nicht in _rendering trifft
grep -rn "\._render\b" --include="*.py" .
# Konstruktor-Signatur einer Klasse
grep -rn "SessionBridge(" --include="*.py" .
# Attribut-Zugriff (Achtung: `.type` ist zu generisch — qualifiziere mit Kontext)
grep -rn "project\.type\b\|self\.type\b" --include="*.py" .
Drei Regeln für den Sweep:
\b) sind Pflicht. Sonst trifft \._render auch \._rendering.tests/ mit durchsuchen.README.md, docs/, tools/ ebenfalls scannen.Lernreise 1 hat dich gelehrt, AST-Wächter zu bauen. Genau hier ist die Anwendung: ein Wächter gegen undefiniertes self.method() fängt alles, was der grep übersehen hat — Callbacks, dynamisch konstruierte Aufrufe, falsch escapte Treffer.
Faustregel: Refactor + AST-Wächter + Test-Run = Vertrauen.
Vor dem Commit:
git blame in einem Jahr verständlich bleibt)?Erst dann committen. Atomar — eine Schnittstellen-Änderung pro Commit.
Refactor-Disziplin verhindert deine eigenen Bugs. Die Welt schickt dir aber auch Eingaben, die du nicht kontrollierst — YAML-Dateien, JSON-APIs, User-Eingaben.
Die Regel: Vertrauen innen, validieren am Rand.
Erinnerung: description durfte plötzlich dict sein, der Vertrag versprach Optional[str].
Möglichkeit 1 — Schaden umschließen:
try:
return description.lower()
except AttributeError:
return ""
❌ Das Problem wandert, statt sich aufzulösen. Jede zukünftige Konsumstelle muss denselben try/except schreiben — oder vergessen, und der Bug taucht woanders wieder auf.
Möglichkeit 2 — Vertrag aufweichen:
@dataclass
class Project:
description: Union[str, dict, None]
❌ Jede Konsumstelle muss jetzt drei Fälle behandeln. Der Code im Innern wird komplexer — wegen einer Eigenheit am Rand. Das ist die Komplexität an der falschen Stelle.
Möglichkeit 3 — Am Rand zwingen:
def _create_project(self, metadata: dict) -> Project:
description = metadata.get("description")
if isinstance(description, dict):
# Sprachvarianten → eine kanonische Form
description = description.get("en") or next(iter(description.values()), None)
return Project(description=description, ...)
✅ Der Vertrag bleibt unverletzt — description: Optional[str] — und alle Konsumstellen bleiben einfach.
Genau eine Stelle pro Grenze zwingt die Eingabe in die kanonische Form. Alle anderen Stellen dürfen vertrauen.
Wenn du dieselbe Coercion-Logik an zwei Stellen hast, ist eine der beiden Stellen falsch oder du hast eine Grenze übersehen.
Such in deinem Code eine Stelle, an der eine Datenklasse Eingaben aus einer Datei oder API erhält. Frag dich:
Schreib einen Coercion-Block am Rand. Ergänze einen Test mit einer „dreckigen" Eingabe. Beobachte, wie der Test grün bleibt, obwohl sich am Innern nichts geändert hat.
self._render" in 0,3 Sekunden findet. Die statische Absicherung deines Refactor-Sweeps.description-Coercion in _create_project)dataclasses, typing.Optional