SurveyIndex + SurveyRuntimeHelper – Anatomie eines God Flow

Wenn zu viel Verantwortung nicht in einer Klasse steckt, sondern in einem Ablauf

Wenn zu viel Verantwortung nicht in einer Klasse steckt, sondern in einem Ablauf

In den letzten Artikeln dieser Serie haben wir zwei bekannte Antipattern betrachtet: createFieldMap() als Beispiel einer Funktion, die zu viele Verantwortlichkeiten in sich vereint, und LimeMailer als Beispiel einer Klasse, die zu viel weiß, zu viel hält und zu viel tut. Beide Diagnosen folgten derselben Logik: Zu viel Verantwortung an einer Stelle.

Diesmal geht es um etwas anderes. Nicht eine Funktion. Nicht eine Klasse. Sondern einen Ablauf.


Zwei Klassen, ein Ablauf

Bevor wir ins Detail gehen, lohnt ein Blick auf die nackten Zahlen. SurveyIndex.php hat 751 Zeilen. SurveyRuntimeHelper.php hat 1963 Zeilen. Zusammen fast 2.800 Zeilen für einen einzigen Ablauf – die Teilnahme an einer Umfrage.

Beide Klassen sind für sich genommen bereits God Classes. Das eigentliche Problem liegt aber tiefer: sie sind nicht unabhängig. Sie implementieren gemeinsam einen einzigen Use Case, der ohne die jeweils andere Seite nicht funktioniert. Das macht sie zu etwas anderem als zwei große Klassen – dazu später mehr.


Der Einstieg

Wer den Teilnahme-Workflow von LimeSurvey zum ersten Mal liest, stößt irgendwann auf diesen Ausschnitt:

$redata = compact(array_keys(get_defined_vars()));

$tmp = new SurveyRuntimeHelper();
$tmp->run($surveyid, $redata);

Das sieht aus wie ein normaler Methodenaufruf. Ist es aber nicht. compact(array_keys(get_defined_vars())) serialisiert den lokalen Scope – jede Variable, die zu diesem Zeitpunkt existiert, landet in $redata und wird Teil der Übergabe. Der Vertrag zwischen Aufrufer und Aufgerufenem ist damit nicht mehr in einer Signatur ablesbar. Er ist implizit, vollständig und gefährlich: jede neue lokale Variable ändert potenziell das Verhalten von run(), ohne dass irgendjemand das explizit entschieden hat.


SurveyIndex – der Controller, der keiner ist

Die Klasse heißt SurveyIndex extends CAction. Der Name suggeriert einen dünnen Controller – einen Einstiegspunkt, der einen Request entgegennimmt und weiterleitet. 751 Zeilen, 33,5 KB. Schaut man sich action() an, passiert dort deutlich mehr als ein dünner Controller rechtfertigen würde. Bevor SurveyRuntimeHelper::run() überhaupt aufgerufen wird, übernimmt SurveyIndex bereits: Request-Parameter werden ausgelesen und validiert, Tokens werden aus GET, POST und Session aufgelöst, die Session wird initialisiert oder zurückgesetzt, der Maintenance-Mode wird geprüft, Preview-Rechte werden validiert, der Survey-Status wird geprüft – existiert er, ist er aktiv, hat er begonnen, ist er abgelaufen –, die Sprache wird gesetzt, gespeicherte Antworten werden geladen, Resume-Logik wird ausgeführt, und Session-Timeouts werden behandelt. Plugins werden getriggert.

Das ist kein dünner Controller. Das ist bereits ein erheblicher Teil des Teilnahme-Workflows, bevor der eigentliche „Helper" überhaupt beginnt.


SurveyRuntimeHelper – der Kern, der sich Helper nennt

Der Name suggeriert eine Hilfsklasse – etwas, das unterstützt. 1963 Zeilen, 93,6 KB. SurveyRuntimeHelper::run() macht etwas anderes: sie orchestriert den gesamten Rest des Teilnahmeprozesses. Navigation durch die Umfrage, Validierung von Antworten, Pflichtfragen-Logik, Datei-Validierung, Quotenprüfung, Datenschutz-Checks, Save/Load/Submit, Aufbau der View-Datenstruktur, Rendering via Twig, Completion-Flow inklusive Redirects und Notifications.

Das ist kein Helper. Das ist der Runtime-Kern des Systems.


Was ein God Flow ist

Bei LimeMailer war die Diagnose: eine Klasse hat zu viele Verantwortlichkeiten. Das Problem war lokal.

Hier ist es anders. Die Verantwortlichkeiten sind auf zwei Klassen verteilt – aber der Ablauf ist trotzdem ein einziger, monolithischer Block. Zugriffskontrolle, Session-Lifecycle, Token-Handling, Resume-Logik, Preview-Modus, Navigation, Validierung, Fehlerbehandlung, Datenaufbereitung, Abschluss und Plugin-Integration laufen nacheinander, implizit aneinandergebunden, ohne klar definierte Übergabepunkte.

Das ist ein God Flow: ein Ablauf, der mehrere Use Cases vermischt, impliziten Zustand zwischen Schritten transportiert, keine explizite Schnittstelle hat, über mehrere Klassen verteilt ist – aber nur als Ganzes funktioniert. Im Unterschied zur God Class ist das Problem nicht die Größe einer einzelnen Klasse, sondern die fehlende Modellierung des Ablaufs selbst. Der Flow existiert, er funktioniert – aber er ist nirgendwo als eigener Baustein sichtbar.

Und compact(array_keys(get_defined_vars())) ist das sichtbarste Symptom davon: wenn der Scope selbst zum Kommunikationskanal wird, gibt es keine Schnittstelle mehr.


Was die LimeSurvey-Entwickler selbst sagen

Das Bemerkenswerteste ist vielleicht das: das Problem ist bekannt. Nicht von außen beobachtet – sondern intern dokumentiert. In SurveyIndex::action() steht direkt über der Methode:

// todo: this function is toooo long, to many things happening here. Should be refactored asap!

Das asap stammt aus dem Code selbst. Wie dringend es tatsächlich war, beantwortet git blame.

Im Quellcode von SurveyRuntimeHelper geht die Selbstdiagnose noch einen Schritt weiter:

In the 2.x version of LimeSurvey and priors, the main run method was
using a variable called redata fed via get_defined_vars. It was making
hard to move piece of code to subfunctions.
Those private variables are just a step to make easier refactorisation
of this file, to have a global overview about what is set in this
helper, and to move easily piece of code to new methods.
It's just a first step. get_defined_vars should be removed, and most of
the private variables here should be moved to the correct object:
i.e: all the private variable concerning the survey should be moved to
the survey model and replaced by a $oSurvey

Das ist Diagnose, Behandlungsplan und Eingeständnis in einem. Die privaten Instanzvariablen sind kein Design – sie sind ein Übergangszustand. get_defined_vars() soll weg. Der Scope soll in echte Objekte überführt werden. Der Kommentar weiß genau wohin die Reise gehen soll.

Was er nicht sagt: wann. Und darin ähnelt er dem "refactor later" aus createFieldMap() – einem Kommentar aus dem Jahr 2012, der PHP 5.x, PHP 7.x und PHP 8.x überlebt hat. Manchmal ist das Benennen eines Problems der erste Schritt. Manchmal bleibt es der einzige.


Der Weg heraus

Das Ziel ist nicht ein Rewrite. Das Ziel ist, den Flow sichtbar zu machen – und damit handhabbar.

Ein erster Schnitt würde den Ablauf in klar benannte Bausteine zerlegen:

$request = SurveyParticipationRequest::fromGlobals();

$accessResult = $accessGuard->check($request);
if ($accessResult->denied()) {
    return $renderer->renderDenied($accessResult);
}

$runtimeState = $orchestrator->run($request);

$viewModel = $viewModelBuilder->build($runtimeState);

return $renderer->render($viewModel);

Hinter diesen zwölf Zeilen stecken mehrere eigenständige Verantwortlichkeiten: SurveyParticipationRequest kapselt alle Eingaben und macht die impliziten Scope-Variablen explizit. SurveyAccessGuard übernimmt Zugriffskontrolle, Token-Auflösung und Status-Prüfung – alles, was heute in SurveyIndex::action() vor dem eigentlichen Ablauf passiert. SurveyRuntimeOrchestrator orchestriert Navigation, Validierung und Persistenz, ohne selbst zu rendern. SurveyViewModelBuilder bereitet den View-State auf. Das Rendering bleibt davon vollständig getrennt.

Diese Zerlegung muss nicht auf einmal passieren. Der erste sinnvolle Schritt ist SurveyParticipationRequest: die implizite $redata-Übergabe durch ein typisiertes Objekt ersetzen. Das allein verändert das Verhalten nicht – aber es macht sichtbar, was der Ablauf tatsächlich braucht. Und was sichtbar ist, lässt sich benennen, testen und schrittweise verbessern.

Das deckt sich übrigens genau mit dem, was der Kommentar im Quellcode fordert: die privaten Variablen in die richtigen Objekte überführen. SurveyParticipationRequest wäre genau das – ein erster konkreter Schritt in die beschriebene Richtung.


Fazit

SurveyIndex und SurveyRuntimeHelper sind nicht schwierig, weil sie alt sind. Sie sind schwierig, weil der Ablauf, den sie implementieren, nie explizit modelliert wurde. Use Case reiht sich an Use Case, Verantwortlichkeit an Verantwortlichkeit – und der Scope selbst wird zum Kommunikationskanal.

God Flows entstehen nicht plötzlich. Sie entstehen, wenn neue Anforderungen immer wieder in denselben Ablauf eingebaut werden, ohne den Ablauf selbst zu hinterfragen. Irgendwann ist nicht mehr die Klasse das Problem – sondern der unsichtbare Faden, der sie zusammenhält.