MV-Patterns – Frontend-Designs in Aktion
Das Frontend einer Software zeigt Informationen an und interagiert mit dem User. Diese Aufgabe ist anspruchsvoll und komplex. Schwierige fachliche Zusammenhänge und Abläufe sollen intuitiv bedienbar sein und strukturiert angezeigt werden. Gleichzeitig sollen diese so implementiert werden, dass sie leicht zu warten und einfach zu testen sind. Zuguterletzt soll die Anwendung stabil und fehlerunanfällig laufen. Die sogenannten MV-Pattens können bei richtiger Anwendung dabei helfen diese Anforderung umzusetzen. In der Realität sieht man leider oft Frontends, welche Schwierigkeiten haben, die Anforderungen zufriedenstellend zu erfüllen. Häufige Fehlerquellen sind Wissenenslücken oder ein unterschiedliches Verständnis von Begriffen unter den Entwicklern. Am Ende setzt oft jeder Entwickler seine eigene Interpretation eines MV-Patterns um. Grund genug, sich die verschiedenen MV-Patterns einmal genauer anzusehen.
Modellierung, Präsentation & Steuerung
Beim Implementieren eines Frontends muss sich der Entwickler mit den folgenden drei Aufgabenbereichen auseinandersetzen:
- Fachliche Modellierung: Fachliche Modellierung der anzuzeigenden Daten und Geschäftslogik.
- Präsentation: Darstellung der Daten und Realisierung der Benutzerinteraktionen.
- Steuerung: Navigation und Reaktion auf Benutzerinteraktionen.
Diese drei Bereiche lassen sich unabhängig voneinander implementieren. Für die Präsentation kann zum Beispiel ein leerer Button definiert werden. Damit der Button mit Leben gefüllt wird, braucht er ein anzuzeigendes Label und eine beim Klicken auszulösende Aktion. Die Fachliche Modellierung kann davon getrennt eine Geschäftslogik und einen String vorsehen. Die Steuerung bringt die beiden schließlich zusammen, indem sie die Präsentation mit der Fachlichen Modellierung verbindet.
Dieses Vorgehen hat viele Vorteile. Zum einen Wiederverwendbarkeit. Soll an anderer Stelle erneut ein Button angezeigt werden, so kann der "leere Button" erneut verwendet und mit einem anderen Label und einer anderen Geschäftslogik verbunden werden. Auf der anderen Seite kann auch die Geschäftslogik durch eine andere Aktion ausgelöst werden und ist nicht zwangsläufig an den Button gebunden.
Zweitens entsteht besser testbarer Code. Die Implementierungen können nun getrennt voneinander getestet werden. Der erste Test kann überprüfen, ob der Button richtig dargestellt wird und anklickbar ist. Da der Button keine Geschäftslogik kennt, kann der Test auch nicht auf Grund eines Fehlers in der Geschäftslogik fehlschlagen. Ein zweiter Test überprüft, ob die Geschäftslogik korrekt funktioniert. Dazu wird im Test kein Button benötigt. Ein dritter Test kann sicherstellen, dass bei Klick auf den Button auch die richtige Aktion ausgeführt wird.
Drittens erhöht sich die Wartbarkeit des Codes. Dies geschieht bereits durch die bessere Testbarkeit. Außerdem verringert man das Risiko unerwünschter Nebeneffekte deutlich. Möchte man etwas in einem Bereich ändern, bleiben die anderen beiden unangetastet. Der Entwickler kann so bei einer neuen Anforderung identifizieren, welchen der drei Bereiche die Änderung betrifft und gezielt eine Änderung vornehmen. Er muss sich nicht erst durch den Code eines anderen Bereichs kämpfen, welcher für die Umsetzung seiner Aufgabe nicht relevant ist. Außerdem vermeidet er Nebeneffekte, die durch intransparente Abhängigkeiten entstehen.
Die Aufteilung der Aufgabenbereiche in drei unabhängige Komponenten erhöht also die Wartbarkeit, die Testbarkeit und die Wiederverwendbarkeit des Codes. Dies scheinen lohnende Ziele in der Softwareentwicklung zu sein. Andererseits wirft die Aufteilung in verschiedene isolierte Komponenten zwangsläufig die Frage nach der Kommunikation zwischen den Codeteilen auf. In der Praxis wird diese oft scheinbar durch die reine Benennung der Bereiche nach dem bekannten Model-View-Controller-Pattern gelöst. Eine falsche Anwendung des Patterns gefährdet jedoch in vielen Fällen die oben genannten Ziele. Die Folge sind durch die Aufteilung zusätzlich entstehende Kommunikationsaufwände ohne den erhofften Benefit einzustreichen. Außerdem gibt es neben dem MVC-Ansatz noch andere Patterns. Viele Frameworks setzen heutzutage als Architekturstil auf Patterns wie MVP oder MVVM. Das Arbeiten entgegen der vorgegebenen Patterns bringt oftmals Schwierigkeiten mit sich und führt ebenfalls zur Verfehlung der oben beschriebenen Ziele. Um den größtmöglichen Mehrwert aus der Aufteilung seines Frontends herauszuholen, macht es also Sinn, sich für ein möglichst passendes Architekturmuster zu entscheiden.
Bei den verschiedenen Patterns fallen zunächst die Gemeinsamkeiten auf. Schon in den Namen finden sich deckungsgleich die Begriffe Model & View wieder. Die Fachliche Modellierung bildet den Anwendungskern und besteht aus zwei Teilen. Die fachliche Ausmodellierung der Daten passiert im so genannten Model. Hier wird festgelegt, welche Daten es gibt und in welchem Zusammenhang sie zueinander stehen. In kleineren Anwendungen wird die Geschäftslogik in die Models selbst implementiert. Ansonsten kann sie separat ausmodelliert werden, zum Beispiel in (ausführbaren) Use Cases, Services oder Interaktoren, welche dann ihrerseits auf den Models arbeiten. Wichtig ist hierbei, dass der Anwendungskern unabhängig vom Rest der Anwendung implementiert wird. Auf diese Weise ist die Geschäftslogik isoliert von anderen Komponenten test- und wiederverwendbar. Die Präsentation wird durch so genannte Views repräsentiert. Sie stellt die vom Benutzer angefragten Informationen dar und gibt ihm die Möglichkeit, mit der Anwendung zu interagieren. Je nach Pattern und Ausprägung kann die View mal "schlauer" und mal "dümmer" ausfallen. Die größten Unterschiede zwischen den MV-Patterns gibt es bei der Steuerung. Hier werden Model & View auf verschiedene Weisen in Zusammenhang gebracht. Jedes Pattern folgt dabei einem eigenen Grundansatz, welcher das Zusammenspiel prägt. Im Folgenden werden diese genauer beleuchtet.
MVC (Model-View-Controller)
Der Klassiker unter den Frontend-Designs ist das Model-View-Controller-Pattern (MVC-Pattern, s. Abb. 1). Die Aufteilung in die drei Klassen entspricht unmittelbar der Aufteilung in die drei Aufgabenbereiche. Der Controller enthält die Logik zur Steuerung und hat direkte Abhängigkeiten zu View und Modell. Für den Fall einer Benutzerinteraktion gibt es zudem eine lose Kopplung vom View zum Controller. Diese kann zum Beispiel über ein Observer oder ein Event Bus Pattern realisiert werden. Eine direkte Abhängigkeit des Views zum Controller – zum Beispiel in Form einer direkten Referenz – gibt es nicht. Wird der Controller über eine Benutzerinteraktion informiert, kann er geeignet darauf reagieren. Durch Aufruf der Geschäftslogik können Daten im Model geändert werden. Hier liegt schon die erste Fehlerquelle verborgen. Der Controller sollte die Daten im Model nicht direkt ändern, da dies bereits eine Geschäftsentscheidung wäre. Vielmehr ist er ein Bindeglied zwischen Geschäftslogik und User Interface. Er ruft lediglich die zum Benutzerevent zugehörige Geschäftslogik auf und überlässt dieser die Entscheidung über Datenänderungen. Die direkte Manipulation der Daten im Model führt oft zu unerwünschten Seiteneffekten in der Fachlichen Modellierung. Mehrere Aufrufe auf dem Model in einer Anfrage oder das direkte Lesen und Schreiben von Daten im Model sind Symptome dafür, dass der Controller sich in die Geschäftslogik einmischt.
Desweiteren ist er für die Navigation zwischen verschiedenen Views verantwortlich. Er kann eine View anpassen, austauschen oder zu einem anderen Controller wechseln. Um Models nicht zwischen den Controllern herumreichen zu müssen, steuert ein Controller alle Views des gleichen Models.
View n:1 Controller
Wie bereits beschrieben, ist das Model vollständig unabhängig von View und Controller. Damit ist auch die Geschäftslogik unabhängig von technischen Implementierungen und kann separat ausmodelliert und getestet werden. Die View ist hingegen abhängig vom Model. Sie kann Daten direkt aus dem Model auslesen, um diese anzuzeigen und Benutzereingaben direkt ins Model zu schreiben. Wenn sich Daten im Model ändern, muss die View über die Änderung informiert werden, um sich anzupassen. Dies kann durch einen Hinweis des Controllers geschehen, dass Daten veraltet sind und neu geladen werden müssen. Eine andere Möglichkeit ist eine lose Kopplung des Models an die View, z. B. in Form eines Observer-Patterns. Auf diese Weise wird eine View über jede Änderung im Model informiert und kann sich entsprechend aktualisieren. Der Datenfluss findet im MVC-Pattern direkt zwischen Model und View statt. Folglich muss die View Logik zum technischen Validieren und Konvertieren von Daten implementieren. In der Praxis bieten viele Frontend-Frameworks ein Data Binding zwischen Model und View an, welches auch diese Funktionalitäten abdeckt. Fachliche Validierungen sind hingegen Teil der Geschäftslogik. Durch den User ausgelöste Änderungen an der View – wie zum Beispiel das Einblenden zusätzlicher Felder nach Betätigung eines Häkchens oder das Einblenden eines Untermenüs – können von der View selbst geregelt werden.
Bei serverseitigen Web-Technologien wie JSPs oder Rails entsteht die lose Kopplung zwischen View und Controller bereits auf technischer Ebene durch das Http-Protokoll. Zudem ist es oft nicht sinnvoll, eine durch den Benutzer ausgelöste Änderung in der View dem Server zu melden, wenn dieser in der Folge nichts Weiteres tut, außer der View zu sagen, wie sie sich anpassen soll. In dem Fall ist es sicherlich naheliegend, die Benutzerinteraktion direkt clientseitig zum Beispiel via JavaScript zu behandeln. Um das Ziel der Wartbarkeit zu erhalten, sollte die Implementierung von Geschäftslogik in der View aber unbedingt vermieden werden.
MVP (Model-View-Presenter) – Passive View
Eine Schwäche des MVC Pattern ist die Mächtigkeit der View. Durch den direkten Zugriff auf das Model kommt oft (unbewusst) eine Menge Logik mit in die View. Diese ist dann nur sehr schlecht test- und wiederverwendbar, weil die Logik meist mit den optischen Elementen verwoben ist. Neben View-interner Logik, Validierung und Konvertierung landet außerdem oft Geschäftslogik in der View. Zudem bleibt der Controller oft sehr passiv, wenn die View Geschäftslogik auf dem Model direkt aufrufen kann und beschränkt sich auf die Navigation. Bei gewachsenen Projekten verschwimmen so die Grenzen der Aufgabenbereiche. Im schlimmsten Fall wird die Geschäftslogik über Model, View und Controller verteilt und die Komponenten verwachsen miteinander.
Im Gegensatz dazu versucht das Model-View-Presenter-Pattern (MVP, s. Abb. 2) die View "dumm" zu halten. Sie soll so wenig Logik wie möglich enthalten. Dies macht die Logik leichter testbar und die optischen Elemente besser wiederverwendbar. Zu diesem Zweck wird alle Logik vollständig aus der View entfernt und der Zugriff auf das Model untersagt. Da diese Logik sehr view-spezifisch ist, erscheint es nun aber wenig sinnvoll, diese in einen Controller zu verlegen, der sich an einem Model orientiert und mehrere Views bedient. Stattdessen übernimmt hier ein so genannter Presenter die Steuerungsaufgaben. Jeder Presenter ist verantwortlich für genau eine View. Wie der Controller ist er verantwortlich für die Navigation und Verarbeitung von Benutzerinteraktionen. Da die View jedoch unabhängig vom Model implementiert wird, muss der Presenter die Daten selbst aus dem Model entnehmen und der View in passend konvertierter Form bereitstellen. Hatte im MVC-Pattern die View eine lose Kopplung zum Model, um auf eine Änderung der Daten zu reagieren, so besteht diese lose Kopplung im MVP-Pattern zwischen Presenter und Model. Registriert der Presenter eine Änderung der Daten, ist er dafür verantwortlich, die Daten aufzubereiten und die View zu aktualisieren.
View 1:1 Presenter
In die andere Richtung hat jede View ihre eigene Presenter-Instanz. Auch wenn es im MVP-Pattern durchaus möglich ist, Presenter und View direkt gegenseitig voneinander abhängig zu implementieren, ist es ratsam, dem Presenter ein Interface vorzuschalten (s. Abb. 3). Dieses enthält dann alle Benutzerinteraktionen, welche die View an den Presenter weiterreichen kann. Dadurch kann die View dann unabhängig vom Presenter implementiert und besser wiederverwendet werden. Da die View keine Kenntnis vom Model hat, bekommt der Presenter im Fall eines Aufrufs die rohen Benutzereingaben und muss diese technisch validieren und konvertieren bevor sie der Geschäftslogik übergeben werden können.
MVP (Model-View-Presenter) – Supervising Controller
Die große Stärke des MVP-Patterns ist die Unabhängigkeit zwischen View und Model, welche eine klare Trennung der Aufgabenbereiche ermöglicht. Auf der anderen Seite bringen viele Frontend-Frameworks – wie z. B. Wicket oder Vaadin – ein gutes Data Binding zwischen View und Model mit. Auf eine komplett passive View ohne Kenntnis des Models zu setzen heißt, hierauf verzichten zu müssen. Dies bedeutet einen unverhältnismäßig höheren Aufwand für den Entwickler, der sich um vieles selbst kümmern muss, was das Framework mit dem Data Binding bereits mitbringt. Außerdem kann der Presenter vor lauter Vermittlungslogik zwischen View- und Modelfeldern schnell unübersichtlich groß werden, ohne dabei schwerwiegende Logik zu enthalten.
Für dieses Problem gibt es eine Art Kompromiss zwischen einer komplett passiven View auf der einen und einer unklaren Aufgabentrennung auf der anderen Seite. Das MVP-Supervising-Controller-Pattern (s. Abb.4) hebt die strikte Trennung zwischen Model und View im Fall von Data Binding auf. Das Model wird vom Presenter direkt der View übergeben und per Data Binding gebunden. Hierbei ist es wichtig, dass das Model vom View nach der Initialisierung nicht für die komplette View verfügbar gemacht wird, sondern der View ausschließlich zur Initialisierung des Data Bindings zur Verfügung steht. Somit ist nach der Initialisierung jede Operation außerhalb des Data Bindings nur noch über den Presenter möglich.
Auf diese Weise verschwindet die Validierungs- und Konvertierungslogik wieder im Data Binding. Diese kann normalerweise relativ einfach über Integrationstests zwischen View und Model getestet werden. Auf der anderen Seite bleibt immer noch eine von Logik befreite View.
MVVM (Model-View-ViewModel)
Ein weiterer Nachteil des MVP-Patterns ist die schwierige Wiederverwendbarkeit der Presenter-Funktionen. Soll zum Beispiel ein Wert auf mehreren Views angezeigt werden oder sollen mehrere Views auf dem gleichen Model arbeiten, muss unter Umständen viel Code dupliziert oder in Helferklassen ausgelagert werden. Das Model-View-ViewModel-Pattern (MVVM, s. Abb. 5) adressiert genau dieses Problem.
Im Gegensatz zu einem Presenter ist ein ViewModel nur noch für einen einzigen oder eine kleine Gruppe von Werten und Benutzer-Interaktionen zuständig und kapselt alleine deren Logik. Dabei wird neben dem Model auch jeder anzuzeigende Wert direkt gehalten. Dieser wird dann von der View über ein Observer-Pattern beobachtet und abgegriffen. In der Praxis besteht hier ein Data Binding zwischen der View und dem ViewModel. Bei diesem ViewModel können sich jetzt beliebig viele Views anmelden und somit alle den gleichen aktuellen Wert anzeigen. Um alle Felder anzeigen zu können wird eine View unter Umständen mehrere ViewModels beobachten.
View n:m ViewModel
Ein kleines Beispiel (s. Abb. 6). Ein Model Warenkorb enthält Produkte mit einem Preis und bietet eine Methode, um den WarenkorbWert zu ermitteln. Das ViewModel WarenkorbWertViewModel meldet sich als Observer beim Model an und wird benachrichtigt, wann immer die Liste der Produkte verändert wird. In dem Fall holt es sich den neuen WarenkorbWert und aktualisiert das entsprechende Feld. Dieses wiederrum ist per Data Binding oder ebenfalls über ein Observer-Pattern mit der View verbunden. Ändert sich also die Liste der Produkte im Model, wird sofort ein neuer WarenkorbWert errechnet und unmittelbar aktuell auf allen registrierten Views angezeigt.
Im Gegensatz zu einem Presenter oder einem Controller präpariert ein ViewModel nur Werte und hat keine Kenntnis von den Views, für welche die Werte aufbereitet werden. Die View ist selbstständig dafür zuständig, sich den aktuellen Wert zu holen und auf Kommando zu aktualisieren – ohne jedoch Anwendungslogik zu beinhalten oder auf das Model selbst zuzugreifen. Data Binding oder das Observer-Pattern synchronisiert den Wert im View mit dem im ViewModel und konvertiert und validiert diesen nach Bedarf. Auf diese Weise kann der WarenkorbWert auf der einen View als Zahl (z. B. 200,00 €) und auf der anderen konvertiert zu einem Wort (z. B. "Zweihundert Euro") dargestellt werden. Das ViewModel selbst kümmert sich um die Synchronisierung mit dem Model und arbeitet eingehende Benutzerinteraktionen ab.
Da das ViewModel nichts von irgendwelchen Views weiß und eigentlich nur seine Beobachter über eine Veränderung informiert, besteht natürlich auch die Möglichkeit, dass sich ein anderes ViewModel registriert. Im Beispiel vom WarenkorbViewModel könnte sich zum Beispiel ein GesamtPreisViewModel registrieren, welches den Warenkorbwert inklusive der Versandkosten enthält. Dieses würde ebenfalls über eine Veränderung des WarenkorbWertes informiert werden und seinen Wert ebenfalls neu berechnen und diesen wiederum bei sich zur Verfügung stellen.
Eine Gefahr des MVVM-Patterns ist Unübersichtlichkeit. Es entstehen sehr viele ViewModels bei denen erstmal nicht klar ist, von welchen Views diese überhaupt verwendet werden. Insbesondere wenn ViewModels aufeinander aufbauen besteht die Gefahr, dass sehr aufwändig und im schlimmsten Fall zeitintensiv Werte berechnet werden, die schlussendlich nirgends angezeigt werden. Außerdem besteht die Gefahr, Kreise zu implementieren, welche endlos Werte neu berechnen. Ein solches Frontend-Design fordert dadurch etwas sorgfältigere Dokumentation, um Wildwuchs zu vermeiden. Dem gegenüber stehen kleine Klassen, hohe Wiederverwendbarkeit, klare Aufgabentrennung und gute Testbarkeit des Codes.
Frontend sucht bestes MV-Pattern
Nach eingehender Analyse der verschiedenen Varianten stellt sich abschließend die Frage nach der Auswahl des "richtigen" MV-Patterns. Bedauerlicherweise gibt es hier keine allgemeingültige, immer richtige Antwort. Keine der vorgestellten Alternativen ist a priori besser als die anderen und daher grundsätzlich zu bevorzugen. Vielmehr ist die Auswahl eines passenden Design-Patterns von verschiedenen projektspezifischen Faktoren abhängig. Da es schwierig ist, einfach mal so ein MV-Pattern durch ein anderes zu ersetzen und die Anwendung verschiedener MV-Patterns erfahrungsgemäß chaotisch endet, ist es ratsam, diese Entscheidung geneinsam im Team zu diskutieren und zu fällen. Dabei sollten folgende Aspekte beachtet werden.
- Welche Frameworks werden eingesetzt?
Oftmals bringen eingesetzte Frontend-Frameworks bereits Konzepte mit, auf welche die API des Frameworks zugeschnitten ist. Dies gilt auch für die Wahl des MV-Patterns. Sich für ein anderes MV-Pattern zu entscheiden, will gut überlegt sein und sollte zuvor dringend evaluiert werden. In einigen Fällen kommt es zu Workarounds und sperrigem Code. Im schlimmsten Fall arbeitet man das ganze Projekt über gefühlt gegen das Framework. - Welche fachlichen Anforderungen gibt es?
Die verschiedenen MV-Patterns haben unterschiedliche Stärken und Schwächen. Es ist oftmals von Vorteil, eines auszuwählen, welches gut zu den vorgegebenen fachlichen Anforderungen passt. Wenn zum Beispiel viele inhaltlich gleiche Werte auf vielen Views angezeigt werden sollen, erscheint das MVVM-Pattern ideal. Ist dies aber nicht der Fall, erhält man im Vergleich zum MVP-Pattern lediglich mehr Komplexität, jedoch keinen zusätzlichen Nutzen. - Welche Muster sind im Team bekannt?
Sich für ein Design-Pattern zu entscheiden, welches einige Teammitglieder nicht richtig verstanden haben, ist von vornherein zum Scheitern verurteilt. Das Pattern kann noch so gut zum Projekt passen, bei falscher oder uneinheitlicher Umsetzung entsteht schnell Chaos. Daher ist es wichtig, sicherzustellen, dass jedes Teammitglied das Pattern inhaltlich verstanden hat und dass es im Team ein gemeinsames Verständnis für das Pattern gibt. Um Missverständnisse zu vermeiden, ist es hilfreich, Begriffe deutlich abzuklären und eine gemeinsame Sprache zu entwickeln. Oft ist hierbei ein interner Workshop mit kleinen praktischen Beispielen zur Orientierung zielführend.
Fazit
In diesem Artikel wurde besprochen, wie durch die korrekte Aufteilung von fachlicher Modellierung, Präsentation und Steuerung in unterschiedliche Komponenten die Wart-, Test- und Wiederverwendbarkeit von Frontend-Code erhöht werden kann. Dazu wurden die vier MV-Patterns MVC, MVP (Passive View & Supervising Controller) und MVVM zur Umsetzung einer solchen Aufteilung vorgestellt und deren Vor- und Nachteile besprochen. Abschließend wurden Kriterien zur Auswahl eines geeigneten MV-Patterns diskutiert.