05.09.2026 · Fabricio Ruch

Warum der zwölfte Patch kein Refactoring ersetzt

AutomationProgramming PrinciplesSoftware ArchitectureSoftware EngineeringSystem Engineering

Software wird selten auf einen Schlag unwartbar.

Meist passiert es schleichend.

Ein kleiner Fehler taucht auf.

Man behebt ihn.

Danach entsteht ein Sonderfall.

Auch der wird behoben.

Dann ein weiterer.

Dann noch einer.

Jeder einzelne Patch ist für sich betrachtet nachvollziehbar.

Und trotzdem wird das System mit jedem Schritt schwieriger zu verstehen.

Das Gefährliche daran:

Lokale Korrektheit kann globale Verschlechterung erzeugen.

Genau deshalb ist nicht jeder weitere Fix automatisch die richtige Entscheidung.

Manchmal ist der nächste Patch nur noch ein Symptom dafür, dass die bestehende Struktur ihre Grenze erreicht hat.


Der erste Patch ist fast immer richtig

Am Anfang ist das völlig normal.

Ein Skript funktioniert nicht sauber mit Paging.

Also ergänzt man Paging.

Ein API-Call wird gelegentlich gedrosselt.

Also ergänzt man Retry.

Unicode wird falsch dargestellt.

Also korrigiert man Encoding.

Ein Progressbar-Index wird falsch berechnet.

Also korrigiert man die Formel.

Das sind sinnvolle Änderungen.

Niemand sollte für jeden kleinen Bug sofort ein Refactoring starten.

Problematisch wird es erst dann, wenn sich ein Muster bildet.


Das Muster hinter zu vielen Patches

Ein typischer Verlauf sieht so aus:

Bug
↓
lokaler Fix
↓
neuer Seiteneffekt
↓
lokaler Fix
↓
neue Sonderbedingung
↓
lokaler Fix
↓
noch ein Sonderfall

Irgendwann entsteht so etwas:

if ($x) {
    ...
}
elseif ($y -and -not $z) {
    ...
}
elseif ($retry -and $resume -and -not $quiet) {
    ...
}

Dann folgt vielleicht:

if ($null -ne $value) {
    ...
}

und später:

if ($propertyExists) {
    ...
}

und irgendwann:

if ($isInteractive -and -not $NoAnimation -and $phase -eq 'Processing') {
    ...
}

Jede einzelne Bedingung kann berechtigt sein.

Das Problem ist nicht eine bestimmte if-Abfrage.

Das Problem ist, dass die Struktur immer mehr Sonderwissen aufnehmen muss.


Der entscheidende Punkt: Wechselwirkungen

Technische Schuld zeigt sich oft nicht zuerst durch schlechten Code.

Sie zeigt sich durch Wechselwirkungen.

Eine Änderung an der Darstellung beeinflusst plötzlich die Logik.

Ein Retry verändert den Fortschrittszähler.

Ein Checkpoint beeinflusst Resume.

Resume beeinflusst Verifikation.

Verifikation beeinflusst den Completion Screen.

Der Completion Screen greift wiederum auf Werte zu, die nur in bestimmten Codepfaden gesetzt wurden.

Plötzlich hängt alles an allem.

Das ist ein sehr wichtiges Warnsignal.


Ein Bug ist nicht immer lokal

Angenommen, eine Anwendung zeigt nach erfolgreicher Verarbeitung einen Fehler im finalen Screen.

Technisch ist die Geschäftsoperation bereits erfolgreich abgeschlossen.

Der Fehler liegt nur in der Darstellung.

Ein schneller Fix könnte sein:

if ($null -ne $VerifiedRemaining) {
    ...
}

Das behebt den Fehler.

Aber vielleicht zeigt der Bug etwas Größeres:

Warum kennt der Completion Screen überhaupt Rohdaten aus der Verifikationslogik?

Vielleicht fehlt ein sauber definiertes Result-Objekt.

Zum Beispiel:

$result = [pscustomobject]@{
    Status      = 'Completed'
    Processed   = 56
    Failed      = 0
    Remaining   = 0
    Verified    = $true
}

Dann müsste der Screen nicht mehr wissen, woher diese Werte kommen.

Das ist der Unterschied zwischen:

Bug beheben

und:

Abstraktion korrigieren

Symptome versus Ursache

Ein Patch reagiert häufig auf ein Symptom.

Refactoring reagiert auf die Ursache.

Beispiel:

Symptom:
UI flackert

Patch:

weniger Clear-Host

Neues Symptom:

alte Zeilen bleiben stehen

Patch:

mehr manuelles Clearing

Neues Symptom:

Transitions sehen falsch aus

Patch:

Sonderbehandlung für Zustandswechsel

Irgendwann erkennt man:

Das Problem ist nicht Clear-Host.

Das Problem ist, dass kein Rendering-Modell existiert.

Dann ist die richtige Lösung:

State Transition
→ Full Redraw

Same State
→ In-place Redraw

Das ist Refactoring.


Refactoring verändert die Struktur, nicht das Verhalten

Das klassische Ziel von Refactoring ist:

Struktur verbessern, Verhalten erhalten.

Das bedeutet:

Vorher:

Input A
→ Output B

Nachher:

Input A
→ Output B

Aber intern ist der Code klarer aufgebaut.

Zum Beispiel vorher:

function Invoke-Tool {
    Connect-Service
    Get-Data
    Process-Data
    Save-Checkpoint
    Render-Screen
}

Nachher:

Orchestrator
├── API
├── Processing
├── Checkpoint
├── State
└── UI

Das sichtbare Verhalten bleibt gleich.

Aber Änderungen lassen sich künftig isolierter durchführen.


Der Unterschied zwischen Patch und Refactoring

Ein Patch beantwortet:

Wie behebe ich diesen Fehler?

Ein Refactoring beantwortet:

Warum konnte dieser Fehler hier überhaupt entstehen?

Das ist eine andere Ebene.

Beispiel:

Fehler:
Failed messages werden im Checkpoint als verarbeitet gespeichert.

Patch:

if ($success) {
    Add-ToCheckpoint
}

Das kann funktionieren.

Aber die tiefere Frage lautet:

Warum bekommt die Checkpoint-Funktion überhaupt eine unscharfe Menge „Messages“?

Vielleicht sollte die Batch-Funktion stattdessen explizit liefern:

SucceededIds
FailedIds
RetryIds

Dann wird die Checkpoint-Semantik klar.

Das ist strukturell stärker.


Fehler zeigen fehlende Abstraktionen

Viele Bugs sind Hinweise darauf, dass ein Konzept im Code nicht sauber modelliert ist.

Zum Beispiel:

Paging Bug
→ fehlende zentrale Paging-Abstraktion

Resume Bug
→ unsauberes Zustandsmodell

Completion Bug
→ unsauberes Result-Modell

TUI Flicker
→ fehlendes Rendering-Modell

Retry Chaos
→ fehlende Fehlerklassifikation

Quiet Mode Chaos
→ UI zu stark gekoppelt

Das ist eine nützliche Denkweise:

Nicht nur fragen, welche Zeile falsch ist, sondern welches Konzept fehlt.


Wann sollte man noch patchen?

Nicht jeder Bug verlangt Refactoring.

Patchen ist völlig sinnvoll, wenn:

  • Ursache lokal ist
  • keine neue Kopplung entsteht
  • Verhalten klar bleibt
  • keine neuen Sonderfälle eingeführt werden
  • Struktur weiterhin verständlich ist

Beispiel:

$index = [Math]::Floor($value)

statt einer falschen Berechnung.

Das ist einfach ein Bugfix.

Kein Architekturproblem.


Wann wird Refactoring wahrscheinlicher?

Ein Refactoring ist angezeigt, wenn mehrere dieser Symptome auftreten:

  • derselbe Zustand wird an mehreren Orten verwaltet
  • mehrere Funktionen greifen auf globale Variablen zu
  • neue Features benötigen immer neue Sonderbedingungen
  • UI und Geschäftslogik beeinflussen sich gegenseitig
  • Fehlerbehandlung ist über den ganzen Code verteilt
  • Tests benötigen große Teile des Programms gleichzeitig
  • derselbe Codepfad verhält sich je nach Modus völlig anders
  • Änderungen in einer Datei brechen unerwartet andere Bereiche
  • Funktionen kennen mehr Kontext, als sie eigentlich brauchen

Das sind strukturelle Probleme.


Der zwölfte Patch ist kein magischer Wert

Natürlich gibt es keine echte Regel:

Nach elf Patches muss refactored werden.

Der Titel ist bewusst zugespitzt.

Der Punkt ist:

Wenn sich Fehler zunehmend zwischen bestehenden Lösungen bilden, ist das ein starkes Signal.

Zum Beispiel:

Patch A behebt Paging.
Patch B behebt Retry.
Patch C behebt Resume.

Neuer Bug:
Resume + Retry + Paging erzeugt falschen Zustand.

Jetzt liegt das Problem nicht mehr in einem einzelnen Feature.

Es liegt in deren Zusammenspiel.

Das ist der Moment, in dem weitere lokale Fixes teuer werden.


Technische Schuld ist Zins

Ein bekanntes Bild für technische Schuld ist finanzielle Schuld.

Ein schneller Fix spart heute Zeit.

Dafür entstehen später Kosten.

Ein bisschen Schuld ist völlig normal.

Manchmal sogar sinnvoll.

Zum Beispiel:

Heute:
schnelle Lösung

Später:
Refactoring

Problematisch wird es, wenn der „Zins“ steigt.

Das merkt man daran, dass jede kleine Änderung immer mehr Aufwand verursacht.


Die Änderungssteuer ist ein gutes Maß

Man kann technische Schuld sehr pragmatisch messen:

Wie teuer ist eine kleine Änderung?

Wenn für eine kleine Funktion:

ein weiterer Statuswert anzeigen

plötzlich Änderungen nötig sind in:

API
State
UI
Checkpoint
Quiet Output
Tests
Completion

dann ist die Kopplung hoch.

Ein gutes Design versucht, die Änderungsfläche klein zu halten.


Shotgun Surgery

Ein klassischer Begriff dafür ist Shotgun Surgery.

Eine kleine fachliche Änderung zwingt zu Änderungen an vielen Stellen.

Beispiel:

Neuer Status: Retrying

muss plötzlich ergänzt werden in:

Orchestrator
UI
Logging
JSON
Checkpoint
Animation
Tests

Das kann manchmal berechtigt sein.

Aber wenn es regelmäßig passiert, fehlt oft eine zentrale Abstraktion.


Divergent Change

Das Gegenstück ist Divergent Change.

Eine einzelne Datei muss aus völlig unterschiedlichen Gründen geändert werden.

Beispiel:

Main.ps1

ändert sich wegen:

  • API-Version
  • UI-Design
  • Retry-Verhalten
  • Checkpoint-Format
  • neue CLI-Parameter
  • Verification
  • JSON-Ausgabe

Dann hat die Datei zu viele Verantwortlichkeiten.

Das ist ein starker Hinweis auf notwendige Trennung.


Refactoring ist Risikoreduktion

Refactoring wird manchmal als Schönheitsarbeit betrachtet.

Das ist zu kurz gedacht.

Gutes Refactoring reduziert zukünftige Risiken.

Zum Beispiel:

Vorher:

UI liest direkt API-Daten

Nachher:

API
↓
Result Object
↓
UI

Damit reduziert man die Wahrscheinlichkeit, dass eine API-Änderung direkt den Renderer bricht.

Das ist kein kosmetischer Nutzen.

Das ist Risikomanagement.


Refactoring schafft Grenzen

Eine gute Architektur besteht zu einem großen Teil aus Grenzen.

Zum Beispiel:

API Boundary
State Boundary
Persistence Boundary
UI Boundary
Output Boundary

Innerhalb einer Grenze darf sich Implementierung ändern.

Andere Bereiche sollten davon möglichst wenig merken.

Beispiel:

Graph API

kann intern andere Paging-Logik bekommen.

Die UI sollte davon nichts wissen.

Wenn sie trotzdem angepasst werden muss, ist die Grenze schwach.


Gute Grenzen reduzieren den Radius eines Bugs

Das ist vielleicht der praktischste Vorteil.

Wenn ein Bug in:

Graph.ps1

liegt, sollte er möglichst dort bleiben.

Wenn ein UI-Bug sofort Checkpoint oder Verarbeitung beeinflusst, fehlt Isolation.

Man spricht auch vom Blast Radius einer Änderung.

Gute Architektur versucht, diesen Radius klein zu halten.


Nicht jedes Refactoring muss groß sein

Refactoring bedeutet nicht automatisch:

alles neu schreiben

Das wäre oft sogar gefährlicher.

Gutes Refactoring kann sehr klein sein.

Beispiel:

Step 1:
Paging in eigene Funktion verschieben

Step 2:
Result-Objekt einführen

Step 3:
UI aus Processing lösen

Step 4:
Checkpoint isolieren

Schrittweise.

Mit laufenden Tests.

Ohne Big Bang.


Rewrite und Refactoring sind nicht dasselbe

Ein Rewrite bedeutet:

Wir werfen große Teile weg und bauen neu.

Ein Refactoring bedeutet:

Wir verbessern die Struktur des bestehenden Systems.

Ein Rewrite ist verführerisch.

Der alte Code wirkt schlecht.

Der neue Code beginnt sauber.

Aber ein Rewrite verliert oft implizites Wissen, das im alten Code steckt.

Zum Beispiel:

  • Sonderfälle
  • API-Eigenheiten
  • Fehlerfälle
  • Verhalten in realen Umgebungen

Deshalb ist gezieltes Refactoring häufig sicherer.


Tests machen Refactoring möglich

Ohne Tests ist Refactoring riskanter.

Denn wenn Verhalten erhalten bleiben soll, muss man feststellen können, ob es sich verändert hat.

Ein gutes Testset schafft Vertrauen.

Zum Beispiel:

Paging
Batching
Retry
Checkpoint
Resume
Verification
Quiet Output

Dann kann man intern umbauen und prüfen:

Verhalten gleich?

Wenn ja:

Refactoring erfolgreich.


Aber Tests können schlechte Architektur konservieren

Auch das passiert.

Ein großes Skript kann sehr viele Tests besitzen.

Trotzdem bleibt es schlecht strukturiert.

Dann testen die Tests vielleicht Implementation statt Verhalten.

Beispiel:

Funktion X muss Funktion Y genau zweimal aufrufen.

Dann wird jede interne Änderung teuer.

Besser ist oft:

Input
→ erwartetes Resultat

Tests sollten Refactoring ermöglichen.

Nicht verhindern.


Ein gutes Result-Modell verhindert viele Patches

Ein klassisches Beispiel ist die Rückgabe von Funktionen.

Schlecht:

return $true

Was bedeutet true?

Request erfolgreich?
Alle Items erfolgreich?
Verifiziert?
Keine Fehler?

Besser:

[pscustomobject]@{
    Attempted = 20
    Succeeded = 18
    Failed    = 1
    Retrying  = 1
}

Noch besser, wenn Resume relevant ist:

[pscustomobject]@{
    SucceededIds = @(...)
    FailedIds    = @(...)
    RetryIds     = @(...)
}

Eine präzisere Datenstruktur beseitigt oft ganze Klassen späterer Patches.


Präzise Semantik ist Architektur

Architektur klingt oft abstrakt.

Aber manchmal besteht sie einfach darin, Begriffe sauber zu definieren.

Zum Beispiel:

processed

Was bedeutet das?

gesehen?
versucht?
erfolgreich verändert?
verifiziert?

Wenn diese Bedeutung nicht klar ist, entstehen später Fehler.

Deshalb sind Namen wie:

Attempted
Succeeded
Failed
Remaining
Verified

stärker.

Gute Architektur beginnt oft bei präziser Sprache.


Refactoring sollte Verhalten vereinfachen

Ein guter Umbau reduziert nicht nur Code.

Er reduziert Denkaufwand.

Vorher:

Wenn Quiet und nicht Audit und Resume,
aber nur falls Verification aktiviert...

Nachher:

RunMode
RenderMode
VerificationMode

oder klar getrennte Pfade.

Das Ziel ist nicht möglichst wenig Code.

Das Ziel ist möglichst wenig mentale Zustandskombinationen.


Komplexität verschwindet nicht durch Aufteilen

Ein wichtiger Gegenpunkt:

Mehr Dateien allein sind noch kein Refactoring.

Wenn aus:

Main.ps1

einfach wird:

Part1.ps1
Part2.ps1
Part3.ps1

aber alle weiterhin auf dieselben globalen Variablen zugreifen, wurde kaum etwas gewonnen.

Physische Trennung ist nicht dasselbe wie logische Trennung.

Die Frage lautet immer:

Sind Verantwortlichkeiten wirklich isoliert?


Module sind kein Selbstzweck

Das Gleiche gilt für Module.

Ein Modul kann sauber strukturiert sein.

Oder einfach eine große Datei hinter einem anderen Dateinamen.

Das Ziel sollte sein:

Public API
Private Implementation
klarer State
klare Datenflüsse
geringe Kopplung

Nicht:

möglichst viele Dateien

Der richtige Zeitpunkt ist vor dem Schmerzpunkt

Viele Teams refactoren erst, wenn:

Niemand will diesen Code mehr anfassen.

Dann ist es teuer.

Besser ist früher.

Ein gutes Signal:

Die dritte ähnliche Sonderlösung taucht auf.

Beispiel:

Erster Modus:

Normal

Dann:

Audit

Dann:

Quiet

Dann:

Resume

Wenn jeder Modus neue if-Blöcke quer durch den Code zieht, sollte man früh ein Modusmodell oder klare Schichten einführen.

Nicht erst beim zehnten Modus.


Rule of Three

Eine bekannte Heuristik lautet:

Beim ersten Mal einfach machen.
Beim zweiten Mal beobachten.
Beim dritten Mal abstrahieren.

Das ist keine harte Regel.

Aber sie verhindert zwei Extreme:

zu frühe Abstraktion

und:

endloses Copy-Paste/Patching

Gerade bei kleinen Tools ist das sehr hilfreich.


Architektur sollte der tatsächlichen Komplexität folgen

Ein kleines Skript braucht kein Domain-Driven Design.

Eine einmalige Datenmigration braucht vielleicht keine State Machine.

Ein Tool mit:

API
Retry
Resume
Checkpoint
TUI
Verification
Quiet Mode

braucht dagegen mehr Struktur.

Die Architektur sollte mit dem Problem wachsen.

Nicht davor.

Aber auch nicht zehn Schritte dahinter.


Ein praktischer Refactoring-Check

Wenn ich heute ein gewachsenes Skript analysiere, würde ich diese Fragen stellen:

1. Welche Verantwortlichkeiten existieren?

API
State
UI
Persistence
Output
Error Handling

2. Welche davon sind vermischt?

3. Welche Variable wird von vielen Bereichen verändert?

4. Welche Änderung betrifft unerwartet viele Dateien oder Funktionen?

5. Welche Fehler treten nur durch Kombination mehrerer Features auf?

6. Welche Sonderfälle wiederholen sich?

7. Welche Funktion kennt Dinge, die sie eigentlich nicht kennen müsste?

Diese Fragen zeigen meist ziemlich schnell, wo Refactoring sinnvoll ist.


Der wichtigste Indikator: Erklärbarkeit

Eine überraschend gute Messgröße ist:

Kann ich den Ablauf in wenigen Sätzen erklären?

Wenn die Erklärung lautet:

Wir verbinden uns,
lesen Daten,
bauen eine Work Queue,
verarbeiten sie in Batches,
speichern erfolgreiche IDs,
verifizieren den Endzustand.

ist das gut.

Wenn sie lautet:

Also zuerst ruft das Main Script Funktion A auf,
außer bei Quiet,
dann setzt Funktion B eine Script-Variable,
die Funktion C später liest,
aber nur wenn Resume nicht aktiv ist...

dann beschreibt man nicht mehr das System.

Man beschreibt seine Zufälligkeiten.

Das ist ein starkes Warnsignal.


Der beste Refactoring-Effekt

Der beste Effekt eines guten Refactorings ist nicht:

weniger Zeilen

Es ist:

weniger Überraschungen

Eine Änderung an Retry verändert nicht plötzlich das UI.

Eine Änderung am UI verändert nicht den Checkpoint.

Eine Änderung am Checkpoint verändert nicht die API.

Das System wird vorhersehbarer.

Und Vorhersehbarkeit ist ein zentraler Wert in Software Engineering.


Was wir aus kleinen Tools lernen können

Interessanterweise sind kleine Skripte perfekte Lernobjekte.

Die Architekturprobleme sind dieselben wie bei großen Systemen.

Nur kleiner.

Man sieht sehr deutlich:

globale Zustände
enge Kopplung
unklare Verantwortlichkeiten
fehlende Fehlersemantik

Bei einem großen Enterprise-System verschwinden diese Probleme oft hinter Frameworks und Infrastruktur.

Bei einem 2’000-Zeilen-PowerShell-Tool liegen sie offen da.

Das macht solche Projekte erstaunlich lehrreich.


Der eigentliche Fehler ist nicht der Patch

Patches sind notwendig.

Softwareentwicklung besteht zu einem großen Teil aus Veränderungen.

Das Problem ist nur:

Ein Patch sollte nicht automatisch die Default-Antwort auf jedes neue Problem sein.

Manchmal sollte die Frage lauten:

Wo ist der Bug?

Manchmal aber:

Warum ist dieser Bug überhaupt möglich?

Und manchmal sogar:

Welche Abstraktion fehlt uns?

Das sind unterschiedliche Ebenen.

Gute Softwareentwicklung erkennt, wann man die Ebene wechseln muss.


Fazit

Der zwölfte Patch ist nicht falsch, weil zwölf eine magische Zahl wäre.

Er ist ein Symbol.

Ein Symbol für den Moment, in dem ein System bereits so viele lokale Korrekturen trägt, dass neue Fehler zunehmend aus deren Wechselwirkungen entstehen.

Dann reicht es nicht mehr, nur die nächste Zeile zu korrigieren.

Man muss die Struktur betrachten.

Die entscheidenden Fragen lauten dann:

Welche Verantwortung ist hier vermischt?
Welcher Zustand ist implizit?
Welche Datenstruktur ist zu unscharf?
Welche Grenze fehlt?
Welche Änderung hat einen zu großen Blast Radius?

Refactoring ist genau dafür da.

Nicht um Code schöner aussehen zu lassen.

Sondern um die Struktur wieder an die tatsächliche Komplexität anzupassen.

Die wichtigste Erkenntnis lautet deshalb:

Wenn Fehler zunehmend aus den Lösungen früherer Fehler entstehen, ist der nächste Patch wahrscheinlich nicht mehr die richtige Abstraktionsebene.

Dann ist es Zeit, nicht nur den Fehler zu beheben.

Sondern das System wieder verständlich zu machen.