Model View Controller in Webanwendungen

Was nicht passt, wird passend gemacht – Teil 2
Kommentare

Model View Controller ist eines der bekanntesten Entwurfsmuster. Allerdings ist es mit über dreißig Jahren deutlich älter als das World Wide Web. Wen wundert es da, wenn der Einsatz von MVC im Web das eine oder andere Fragezeichen aufwirft? Wir haben in Teil 1 dieses Artikels schon einmal die Grundlagen abgedeckt und sehen in Teil 2 noch genauer hin.

Wo ist das Model?

Der Controller steht nun ebenfalls vor einem Problem, denn das Model, mit dem er sprechen soll, existiert noch nicht. Und es ist auch gar nicht so trivial, an das Model heranzukommen. Meist muss dazu erst der Request analysiert werden, um daraus eine ID abzuleiten, anhand derer man entscheiden muss, welches Model überhaupt geladen werden soll. Die Tatsache, dass überhaupt ein Model geladen werden muss, spielt im ursprünglichen Verständnis von MVC gar keine Rolle. In einer Webanwendung können wir darauf allerdings nicht verzichten. Es stellt sich aber die Frage, ob es sinnvoll ist, diese zusätzliche Verantwortlichkeit in MVC „hineinzudichten“.

Wir stellen uns vor, dass es einen „Finder“ gibt, der für uns das benötigte Model lädt. Dieser „Finder“ könnte in einer realen Anwendung etwa ein Repository oder ein Table Data Gateway sein. Nach dem Laden des Models kann der Controller nun eine Methode des Models aufrufen, um dort Daten beziehungsweise den Zustand zu verändern.

Jetzt wäre es eigentlich am Model, bei den Views Bescheid zu sagen, dass sich sein Zustand geändert hat. Auf dem Server existieren die Views aber nur als Strings. Erst bei der Darstellung auf dem Client haben wir die Möglichkeit, den Views echtes Verhalten zu geben. View und Model werden in Webanwendungen also durch eine Systemgrenze getrennt. Und da der Client nur per HTTP Daten vom Server abrufen kann, kann das Subject-Observer-Muster hier gar nicht sinnvoll umgesetzt werden; zumindest nicht ohne den Einsatz von JavaScript und AJAX.

Alles, was wir tun können, ist basierend auf den Daten des Models eine View zu erzeugen. Nun wird deutlich, dass im Kontext von Webanwendungen der Begriff „View“ plötzlich eine andere Bedeutung hat als im klassischen MVC-Verständnis: Wir sprechen von einer ganzen HTML-Seite, nicht nur von einem einzigen GUI-Element.

Hinzu kommt, dass wir nicht immer zweifelsfrei wissen, welche Seite wir überhaupt darstellen müssen. Im einfachsten Fall bestimmt nur der angeforderte URL und damit der HTTP-Request die anzuzeigende Seite. In der Realität aber hängt es oft auch vom Zustand der Applikation (ist ein Benutzer eingeloggt oder nicht) oder dem Verarbeitungsergebnis des Requests ab, welche Seite angezeigt werden muss, beziehungsweise ob die angeforderte Seite angezeigt werden darf. Technisch gesehen entspricht der versuchte Zugriff auf eine geschützte Seite einem „Umlenken“ des Benutzers auf eine andere Seite, nämlich die Login-Seite. Eine solche „Umlenkung“ implementiert man am besten durch einen Redirect des Browsers auf die Login-Seite. Damit läuft man nicht Gefahr, Suchmaschinen, die versuchen, geschützte Seiten abzurufen, mit Login-Formularen zu überfluten.

Aber auch ohne geschützte Seiten gibt es das Problem, dass der Request nicht alleine bestimmt, welche Seite anzuzeigen ist. Nehmen wir ein Signup-Formular als Beispiel, in dem ein Benutzer alle Informationen eingibt, die benötigt werden, um sein Benutzerkonto anzulegen. Viele Benutzer werden es nicht auf Anhieb schaffen, alle Daten richtig einzugeben. Wir müssen daher abhängig vom Ergebnis der Validierung entscheiden, ob der Benutzer erneut das Signup-Formular (diesmal mit Fehlermeldungen) sehen soll, oder sein Benutzerkonto erfolgreich angelegt ist und wir ihm seine Profilseite anzeigen. Schon wieder eine neue Verantwortung für unsere MVC-Struktur, die es in der ursprünglichen Fassung nicht gab.

Wenn wir uns einig sind, dass ein Controller für die Verarbeitung eines „Kommandos“ an die Applikation zuständig sein soll, dann sollte er nicht (auch) die Entscheidung fällen, welche Seite anzuzeigen ist. Das wäre nämlich eine Verletzung des Single-Responsiblity-Prinzips. Wenn man beispielsweise die Login-Funktionalität mit einer Entscheidung über die Weiterleitung auf die Profilseite verzahnt, dann ist die Login-Funktionalität nicht mehr ohne weiteres wiederverwendbar. In der Tat liegen viele der Probleme, die Teams längerfristig bei der Wartung von MVC-Anwendungen haben, genau in dieser Verzahnung von einzelnen Funktionalitäten mit dem Workflow der Applikation. Spätestens dann, wenn Controller auch noch Geschäftslogik enthalten, wird es unübersichtlich, denn jetzt ist die Ausführung dieser Geschäftslogik mit der Anzeige von Webseiten verzahnt. Gelingt es dem Benutzer, Seiten in einer unerwarteten Reihenfolge aufzurufen beziehungsweise bestimmte Seiten zu „überspringen“, ist die Applikation plötzlich in einem unerwarteten Zustand, in dem keine mehr so genau vorhersagen kann, wie sie sich verhält. Wirklich schmerzhaft wird die Erkenntnis, dass hier ein schwerer Designfehler gemacht wurde, oftmals erst dann, wenn man etwa mobile Clients an eine vorhandene Applikation anbinden will.

Nochmals: Es ist ein kardinaler Fehler, Geschäftslogik mit der Anzeige von bestimmten Seiten zu verzahnen. Leider ist dieser Fehler allgegenwärtig. Meiner Meinung nach deshalb, weil die gängigen Web-MVC-Frameworks zwar behaupten, Best Practices zu fördern, dem Entwickler aber keine eindeutige Antwort auf zentrale Fragen wie „wie finde ich heraus, welche Seite ein Benutzer als Nächstes sehen soll“ geben. Stattdessen lassen sie den Entwickler an solchen Stellen einfach im Regen stehen.

Was ist eigentlich Logik?

Im Zusammenhang mit der Frage, wo Logik nun eigentlich stehen soll – im Controller oder im Model – hilft es, den recht generischen Begriff „Logik“ einmal genauer zu untersuchen und weiter zu unterteilen. Dazu wollen wir zwischen Domänenlogik, Applikationslogik und Darstellungslogik unterscheiden. Unter Domänenlogik versteht man die (idealerweise) durch die eigentlichen Geschäftsobjekte repräsentierte Geschäftslogik. Hier finden sich die eigentlichen Geschäftsregeln wie etwa „ein Kunde darf höchstens fünf Verträge abschließen“ oder „wenn wir mit einem Kunden in den letzten drei Jahren jeweils mindestens eine Million Euro Jahresumsatz gemacht haben, dann ist er ein Premiumkunde“. Wichtig ist, Domänenlogik sauber von der Applikationslogik zu trennen, denn normalerweise gibt es mehrere Applikationen, die auf einer Domänenlogik basieren. Typischerweise sind das eine Frontend- und eine Backend-Applikation, wobei diese Begriffe (leider) sehr uneinheitlich verwendet werden. Ich verstehe unter Frontend die Applikation, mit der die Endbenutzer arbeiten, also die eigentliche Website selbst, während das Backend die Applikation ist, mit der Redakteure, Moderatoren oder Administratoren die Website beziehungsweise die Inhalte verwalten. Obwohl diese beiden Applikationen mit den gleichen Geschäftsobjekten arbeiten, gelten völlig unterschiedliche Restriktionen, welcher Benutzer wann etwas tun darf. Es ist daher nicht sinnvoll, Applikations- und Domänenlogik zu vermischen.

Präsentationslogik wäre beispielsweise Logik, um aus einer Sammlung von Objekten eine HTML-Tabelle zu erzeugen, deren Spalten bestimmte Attribute dieser Objekte enthalten. Dabei werden normalerweise Formatierungen durchgeführt, etwa um Datumsangaben, Zahlen oder Geldbeträge so darzustellen, wie es der Benutzer in seiner Landessprache gewohnt ist. All dieser Code ist ebenfalls Präsentationslogik und hat in einem Controller nichts zu suchen.

Ob ein Controller nun Applikationslogik enthalten soll oder nicht, hängt davon ab, welche Rolle man einem Controller innerhalb der Applikation zuweist. Auch hier sind die gängigen MVC-Frameworks zu kritisieren, weil sie keine klare Aussage darüber machen (manchmal leider auch eine falsche Aussage), was denn nun die vorgeschlagene Best Practice ist. Falls ein Controller im Sinne der obigen Ausführungen ein einzelnes an die Applikation gesendetes Kommando verarbeitet, dann sollte die Entscheidung über die anzuzeigende Seite und die Erzeugung des entsprechenden Redirects in einem Application Controller stattfinden. Eine redirect- beziehungsweise forward-Methode im Controller ist dann nicht nur unnötig, sondern sogar höchst problematisch.

Um dem Browser einen Redirect-Header zu senden, sollte man nämlich keine Klimmzüge machen müssen. Ein Redirect ist eine ganz normale HTTP-Response, die einen Location-Header enthält. In der Theorie kann der Body dieser Response, also die eigentliche HTML-Seite, leer bleiben. Falls der vom Benutzer verwendete Client allerdings keinen automatischen Redirect unterstützt, ist es sinnvoll, in die Seite einen kurzen Text zu schreiben, der einen Link zu dem URL, auf den umgeleitet werden soll, enthält. Falls Benutzer nicht automatisch weitergeleitet werden, können sie dann immerhin noch manuell den Link klicken, anstelle eine weiße Seite anzustarren.

Da ein solcher Redirect eine ganz normale, wenn auch kurze, gültige HTTP-Response ist, ist aus der Sicht des Frameworks lediglich eine ganz normale Seite zu erzeugen. Und da es im Sinne der obigen Ausführungen nicht die Aufgabe eines Controllers sein darf, über den Redirect zu entscheiden, braucht es dort auch keine entsprechende Methode. Der Controller sollte vielmehr einen sinnvollen Rückgabewert haben, etwa einen booleschen Wert. Darauf basierend kann der Application Controller dann entscheiden, welche Seite als Nächstes anzuzeigen ist. In diesem Szenario muss der Controller dann nicht mehr die View auswählen, sondern führt tatsächlich lediglich Applikationslogik aus. Unabhängig davon, ob der Controller Aufrufe auf weitere Objekte delegiert, ist die Applikationslogik damit von der Anzeige der Seiten entkoppelt und kann unabhängig von der Seitenanzeige wiederverwendet werden.

Alternativ könnte man Controller auch eher als Application Controller begreifen, auch wenn das der ursprünglichen Interpretation von MVC noch mehr zuwider läuft. In diesem Fall wäre der Controller eigentlich „nur“ dafür zuständig, an die auszuführende Applikationslogik (nicht Domänenlogik!) zu delegieren und anhand des Ergebnisses zu entscheiden, welche Seite dem Benutzer als Nächstes anzuzeigen ist. Es entbehrt nicht einer gewissen Ironie, dass diese Art, ein MVC-Framework zu nutzen, besser in existierende MVC-Implementierungen passt als die oben beschriebene, nach der reinen Lehre bessere Variante.

Bevor wir aber weiter abschweifen: Wir haben unseren geistigen Durchlauf von Web-MVC noch nicht beendet. Nehmen wir an, dass wir uns entschieden haben, welche Seite anzuzeigen ist. Diese Seite würde im klassischen Verständnis nun mit Methodenaufrufen im Model dessen Zustand abfragen und die entsprechenden Daten für die Anzeige aufbereiten. In der Realität wird allerdings meist die Information durch den Controller in die View geschrieben. Das ist nicht nur eine völlige Umkehr dessen, was MVC ursprünglich ausgemacht hat, sondern kehrt auch das generelle Prinzip der Interaktion zwischen View und dem Rest der Welt (damit ist je nach Kontext entweder das Model oder der Controller gemeint) um. Während im klassischen MVC die View Daten aus dem Model ausliest (Pull-Prinzip), kommt in den meisten Web-MVCs an dieser Stelle das Push-Prinzip zum Einsatz und Daten werden in die View geschrieben.

Es mag zwar nach Haarspalterei klingen, das zu unterscheiden, aber in der Praxis ergibt sich ein wichtiger Unterschied: Beim Push-Prinzip habe ich keine Garantie, dass die an die View übergebenen Daten dort überhaupt gebraucht werden. Es ist also sehr einfach, vorneweg viel zu viel Arbeit darin zu investieren, eine View mit Daten zu versorgen, die sie gar nicht benötigt. Besonders bei langlebigen Anwendungen sammelt sich hier gerne Overhead an, zumal es keine einfache Möglichkeit gibt, herauszufinden, welche Daten die View tatsächlich braucht. Dieses Problem könnte man übrigens lösen, indem man an dieser Stelle ein explizites API anstelle des meist verwendeten impliziten APIs verwendet. Das bedeutet, dass man anstelle von etwa $view->set(’name‘, ‚value‘); eine explizite Methode schreibt: $view->setName(‚value‘);.

Das ist natürlich mehr Schreibarbeit, ermöglicht es aber später, mit statischer Codeanalyse herauszufinden, welche Daten die View eigentlich braucht. Und wenn man nun schon dabei ist, die Abhängigkeiten aufzuräumen: Warum nicht der View einfach „nur“ ein Model übergeben, das die benötigten Daten bereitstellen kann, es aber der View überlassen, diese dort per Methodenaufruf zu holen. Das setzt dann das an dieser Stelle äußerst sinnvolle Pull-Prinzip richtig um, denn die View wird sich nur die Daten holen, die sie auch wirklich braucht. Damit die View nur ein Model kennen muss, kann man für jede View ein eigenes Presentation Model definieren. Dieses könnte beispielsweise auf verschiedene Domänenobjekte zugreifen, um der View verschiedene Daten zur Verfügung zu stellen. Nun stellt sich noch die letzte große Frage, die MVC offen gelassen hat: Woher kommen diese Domänenobjekte oder, wenn man sie denn so nennen möchte, Models?

Bisher war es ja so, dass unser gerade ausgeführter Controller das eine Model, mit dem er interagieren musste, geladen hat, beziehungsweise durch einen „Finder“ hat laden lassen. Wenn wir nun weitere Models laden müssen, brauchen wir dann dazu weitere Controller? Als Antwort auf diese Frage könnte man den Controller-Stack oder Action-Stack nennen, den viele MVC-Frameworks bieten. Wenn wir doch bloß nicht die Controller mit dem Applikationsworkflow verzahnt hätten, dann könnten wir jetzt an dieser Stelle einfach weitere Controller aufrufen. Dazu müssten wir allerdings in den einzelnen Controllern auch sauber zwischen lesendem und schreibendem Zugriff unterscheiden, und ein if gilt übrigens nicht als sauber.

Alternativ könnte man die Controller nur für den schreibenden Zugriff, also die Kommandoverarbeitung einsetzen. Das Presentation Model wäre dann auch dafür zuständig, die restlichen Models zu laden. Da die View an sich aber „nur“ an Daten interessiert ist, stellt sich die Frage, ob man an dieser Stelle überhaupt noch Models braucht. Hinzu kommt, dass bis auf die Verarbeitung von Kommandos nun eigentlich alles, was spannend ist, außerhalb der MVC-Struktur passiert. Das einzige, was von der ursprünglichen Intention des Musters übrig bleibt, ist „Separate Presentation“. Das allein bekommen wir aber schon, wenn wir die Darstellungslogik konsequent vom „Rest der Welt“ trennen. Braucht es dafür also wirklich MVC?

Fazit

Wir haben gelernt, dass das Problem, das MVC löst – nämlich die Synchronisation unterschiedlicher Views, die gleichzeitig dargestellt werden – für Webanwendungen gar nicht existiert, zumindest nicht im Zusammenspiel zwischen Client und Server. Für One-Page-Applikationen kann es absolut sinnvoll sein, das (ursprüngliche) MVC-Muster in JavaScript vollständig auf dem Client zu implementieren. Eine MVC-Implementierung über Systemgrenzen hinweg ist allerdings nicht sinnvoll. Damit würde man gewissermaßen ein Remote-GUI realisieren, das bei jedem Event über HTTP mit dem Server sprechen müsste. Geschieht dies synchron, reagiert die Anwendung nicht mehr schnell genug; spätestens dann nicht mehr, wenn der Server unter Last gerät. Geschieht dies asynchron, haben wir eine echte verteilte Anwendung und man muss den Zustand der Applikation im Client (JavaScript) mit den Zustand der Applikation im Server (PHP) konsistent halten. Das ist keine leichte Aufgabe, da der Zustand potenziell mit jedem Klick im Browser weiter auseinanderläuft. Eine solche Lösung könnte nur dann sinnvoll funktionieren, wenn man relativ oft die Seite vollständig neu vom Server lädt, um so den Zustand des Browsers wieder mit dem Zustand des Servers zu synchronisieren.

Es ist wichtig zu verstehen, dass sich der ganze Aufwand für einen JavaScript-MVC im Client auch nur dann lohnt, wenn es tatsächlich mehrere gleichzeitig dargestellte Sichten auf die gleichen Daten gibt. Falls eine Anwendung das Problem der View-Synchronisation in der Darstellung gar nicht lösen muss, dann gibt es keinen Grund dafür, MVC zu verwenden.

Auf der Serverseite gibt es an sich keinen Grund dafür, überhaupt noch von MVC zu sprechen. Wir haben gesehen, dass die Komponenten eines Web-MVC mit dem ursprünglichen MVC-Entwurfsmuster nur noch die Bezeichnungen gemeinsam haben. Die einzelnen Bestandteile haben völlig andere Verantwortlichkeiten, und MVC allein gibt keine Antwort auf viele der Fragen, die sich bei der Entwicklung von Webanwendungen stellen. Was das für dein Einsatz von und den Umgang mit MVC bedeuten soll, muss jeder Entwickler beziehungsweise jedes Team für sich selbst entscheiden.

Unsere Redaktion empfiehlt:

Relevante Beiträge

Meinungen zu diesem Beitrag

X
- Gib Deinen Standort ein -
- or -