In Das Framework ist nur ein Detail war unsere These: Neue Anwendungslogik sollte nicht davon abhängen, ob am äußeren Rand Yii, Slim oder ein anderes Framework HTTP-Requests verarbeitet. Das Framework sollte Adapter sein, nicht Architektur.
Daran hat sich nichts geändert. Inzwischen beschäftigt uns allerdings eine Frage, die noch eine Ebene tiefer geht: Wenn wir das Framework aus dem Zentrum von LimeSurvey entfernen – welches Domainmodell wollen wir dort eigentlich erhalten? Begriffe wie QuestionGroup, Question, SubQuestion oder QuestionType sind für LimeSurvey-Entwickler so selbstverständlich, dass man leicht vergisst, sie überhaupt noch infrage zu stellen. Aber sind das notwendige Konzepte eines Survey-Systems – oder historische Architekturentscheidungen, die uns heute nur deshalb wie Domainlogik erscheinen, weil LimeSurvey seit vielen Jahren so funktioniert?
Ein einzelner Commit liefert dafür ein überraschend konkretes Gedankenexperiment.
Ein Commit, eine Frage
Mit PR #5124 (Commit f6c3d658, „Dev #LE-778: Ranking with json type") wurde die Speicherung von Ranking-Antworten grundlegend geändert. Die zugehörige Änderung zieht sich durch 29 Dateien; die interne Datenbankversion steigt von 708 auf 709.
Bisher legte eine Ranking-Frage für ihre Rangpositionen mehrere Spalten in der Antworttabelle an, vereinfacht:
Q123_S1 = C
Q123_S2 = A
Q123_S3 = D
Q123_S4 = B
Jetzt wird die Rangfolge als geordnetes JSON-Array in einer einzelnen Spalte gespeichert:
["C", "A", "D", "B"]
Für Integratoren ist das zunächst ein Breaking Change. SQL-Views, BI-Anbindungen, Plugins oder eigene Exportskripte, die direkt auf den bisherigen Spalten arbeiten, müssen angepasst werden – die Migration erzeugt eine neue Spalte, überträgt die bestehenden Werte und entfernt anschließend die alten.
Architektonisch interessiert uns aber eine andere Frage: Warum brauchte eine Rangfolge überhaupt mehrere Datenbankspalten?
Warum eine Rangfolge keine vier Spalten braucht
Die neue Darstellung ist nicht deshalb natürlicher, weil JSON moderner ist. Sie ist natürlicher, weil eine Rangfolge fachlich eine geordnete Liste ist. Abstrakt beschrieben:
Ranking = list<enum>
Lässt man diesen Gedanken einmal zu, stellt sich die nächste Frage von selbst. Was ist Multiple Choice?
MultipleChoice = set<enum>
Eine Matrix?
Matrix = map<row, value>
Und eine Dual-Scale-Matrix?
DualMatrix = map<row, object { scale1: value, scale2: value }>
Das bedeutet nicht, dass diese Strukturen exakt so in der Datenbank gespeichert werden müssten. Sie beschreiben zunächst nur die fachliche Form der Antwort.
Damit passiert etwas Interessantes: Ranking, Multiple Choice und Matrix sind auf Datenebene plötzlich keine Fragetypen mehr. Sie besitzen ein Response Schema. Der Fragetyp beschreibt dann womöglich gar nicht die Daten, sondern nur noch eine übliche Kombination aus Datenstruktur und Benutzerinteraktion.
Question, SubQuestion – oder Struktur?
Das führt zum nächsten LimeSurvey-Begriff: SubQuestion. Nehmen wir eine einfache Länderauswahl:
COUNTRY
DE
AT
CH
Sind DE, AT und CH SubQuestions? Offensichtlich nicht – es sind mögliche Werte eines Enum. Bei einer Matrix sieht es vertrauter aus:
SATISFACTION
PRICE
QUALITY
SERVICE
In LimeSurvey-Denken sind PRICE, QUALITY und SERVICE Teilfragen. Aus Sicht der entstehenden Daten könnte man aber genauso sagen:
SATISFACTION = map<aspect, integer<1..5>>
PRICE, QUALITY und SERVICE wären dann schlicht Keys dieser Map. Bei einer Dual-Scale-Matrix wird der Unterschied noch deutlicher – die resultierenden Daten sehen etwa so aus:
{
"PRICE": { "importance": 5, "satisfaction": 3 },
"QUALITY": { "importance": 4, "satisfaction": 5 }
}
Der Begriff SubQuestion hilft bei der Beschreibung dieser Struktur nur noch begrenzt. Das heißt nicht, dass es keine fachlichen Teilstrukturen einer Frage gibt – PRICE ist selbstverständlich semantisch ein Teil der Erhebung. Die Frage ist vielmehr, ob jede solche Teilstruktur denselben Domain-Typ SubQuestion haben sollte. Vielleicht ist eine SubQuestion deshalb kein fundamentales fachliches Konzept, sondern eine Abstraktion, die aus der Art entstanden ist, wie LimeSurvey Fragen, Teilfragen und Antwortspalten organisiert.
Die breite Tabelle ist nicht das Problem
An dieser Stelle könnte man leicht zu dem Schluss kommen, LimeSurveys breite Antworttabellen seien grundsätzlich eine schlechte Idee. Das glauben wir nicht. Für Analyse sind breite Tabellen ausgesprochen praktisch:
response_id | AGE | COUNTRY | SAT_PRICE | SAT_QUALITY
SQL-Abfragen, Statistikpakete, BI-Systeme und CSV-Exporte arbeiten hervorragend mit solchen Strukturen. Das Problem entsteht an einer anderen Stelle: In LimeSurvey ist die physische Antworttabelle gleichzeitig Runtime Storage, konkrete Repräsentation der Survey-Struktur, Grundlage für Exporte, Integrationsschnittstelle und Analysemodell. Fünf verschiedene Rollen, eine einzige Struktur.
Wie eng diese Rollen verzahnt sind, zeigt die eigene LimeSurvey-Dokumentation: „MySQL InnoDB: Maximum number of 1000 columns" (MySQL selbst nennt für InnoDB technisch 1017 Spalten, LimeSurvey rundet), und als Faustregel dazu: „every item on the screen that can be filled in or selected costs one column" – ein 10×10-Array kostet demnach bereits 100 Spalten. Wird die Grenze erreicht, bricht die Aktivierung mit einer Fehlermeldung ab, die LimeSurvey selbst ausliefert: „The survey response table could not be created. Usually this is caused by too many (partial) questions in your survey." In der Praxis kann das Limit sogar unterhalb dieser Spaltenzahl greifen, wenn die kombinierte Zeilenbreite unter InnoDB die Grenze überschreitet, unabhängig von der reinen Spaltenzahl.
Das ist keine fachliche Grenze der Umfrage. Es ist eine Eigenschaft der Speicherstrategie, die wie eine fachliche Grenze wirkt, weil Storage und Analysemodell dieselbe Struktur teilen. Eine Grenze der physischen Projektion darf nicht zur Grenze des Domainmodells werden.
Man könnte beides trennen:
Canonical Response Storage → Projection → Analysis Dataset
Während der Durchführung könnte eine Response entsprechend ihrem fachlichen Schema in einem kanonischen Response Store persistiert werden. Ob primitive Werte dabei typisiert und komplexe Strukturen als JSON gespeichert werden oder eine andere physische Repräsentation gewählt wird, wäre eine Entscheidung des Persistence Layers. Entscheidend ist nicht JSON. Entscheidend ist, dass die physische Speicherung nicht mehr das Domainmodell definiert:
Framework ≠ Domain Model
Persistence Model ≠ Domain Model
Presentation ≠ Domain Model
Für Analyse ließe sich aus dem kanonischen Storage weiterhin eine breite Projektion erzeugen – RANKING.rank1, SATISFACTION.PRICE und so weiter. Die breite Tabelle käme also zurück, aber als Read Model, nicht mehr als Source of Truth. Das MySQL-Spaltenlimit würde dann nicht mehr bestimmen, wie komplex ein Survey überhaupt sein darf, sondern nur noch, wie eine bestimmte Projektion aussehen kann. Für den Normalfall bliebe die Wide-Table-Projektion einfach und performant, ohne dass ein außergewöhnlich breiter Survey unmöglich würde.
Fragetypen als Presets
An dieser Stelle könnte der Eindruck entstehen, der Survey Builder dürfe keine Fragetypen mehr anbieten. Das wäre wenig sinnvoll – ein Autor möchte weiterhin zwischen Short Text, Matrix, Ranking oder Dual Matrix wählen. Die Frage ist nur, ob diese Auswahl gleichzeitig das fundamentale Datenmodell bestimmen muss.
Vielleicht ist ein „Fragetyp" im Designer lediglich ein Preset aus drei orthogonalen Teilen: Response Schema, Presentation und Validation. Ranking erzeugt dann ResponseSchema: list<enum> mit Presentation: Ranking, Dual Matrix erzeugt ResponseSchema: map<row, object{scale1, scale2}> mit Presentation: DualMatrix. Der Bedienkomfort bleibt vollständig erhalten, nur intern verändert sich die Bedeutung: QuestionType wäre ein Konzept des Survey Builders, nicht mehr des Persistence Layers.
Das wird bei Fragetypen interessant, die heute gar nicht zum Core gehören. Ein Budget Allocator – „Verteilen Sie 100 Punkte auf Preis, Qualität, Service und Features" – zeigt, warum die dritte Komponente dazugehört:
BudgetAllocator
ResponseSchema:
map<category, integer>
Presentation:
BudgetAllocator
Validation:
sum(values) == 100
Für keinen dieser Fälle müsste die Persistence einen neuen Fragetyp kennen. Ein Plugin müsste lediglich eine neue Presentation, ein Preset und gegebenenfalls einen Validator bereitstellen – nicht aber eine neue Art der Persistenz erfinden. Genau das würde verhindern, dass sich über Jahre wieder neue Storage-Sonderfälle ansammeln. Was wir heute QuestionType nennen, könnte intern eher ein Preset sein, das Response Schema, Presentation und Default-Validation sinnvoll vorkonfiguriert.
Ein Commit als Fenster
Der Ranking-Commit verändert zunächst nur einen einzelnen Fragetyp. Architektonisch öffnet er trotzdem ein erstaunlich großes Fenster: Eine Rangfolge wird zu dem, was sie ohnehin schon war – eine geordnete Liste. Eine Matrix lässt sich als Map beschreiben. Und ein Fragetyp vielleicht nur noch als Preset aus Response Schema, Presentation und Validation.
Das ist noch keine neue Architektur. Aber es ist eine andere Art, auf die bestehende zu schauen.
Das Framework ist nur ein Detail. Das Domainmodell ist es nicht.