Professionelle Anwendungsentwicklung mit MVC
Das MVC (Model View Controller)-Konzept stellt für zukunftsorientierte
Applikationen eine Grundvoraussetzung dar. Es trennt die unterschiedlichen
Applikationslogiken und gibt den Entwicklern einen transparenten
Gesamtüberblick. Zusätzlich sind die einzelnen Komponenten leichter
zu warten, zu erweitern und auszutauschen. Solche Flexibilitäten
sind für mehrschichtige Anwendungen unverzichtbar.
Die Einsatzszenarien der heutigen Softwaresysteme werden immer komplexer. MVC
hilft hierbei, den Überblick zu wahren. Die meisten Applikationen
werden mittlerweile fürs Internet konzipiert und müssen interoperabel
mit verschiedensten verteilten Systemen sein. Dabei kommt eine
Mehrschichten-Architektur zum Tragen, die eine strikte Trennung der verschiedenen
Logiken erfordert. In solch komplexen Applikationsaufbauten stellt der Ansatz
von MW eine Grundvoraussetzung dar. Die Grundidee, die hinter MVC steht,
ist, daß grundsätzlich jede zu realisierende Anwendung aus drei
Teilen besteht: dem Modell, einigen Repräsentationssichten und den
Controllern.
Dabei stellt das Modell den Teil der Applikation dar, der die aktuelle Anwendungslogik beinhaltet. Wenn wir sagen, wir wollen die Benutzerschnittstelle von der Anwendung trennen, so ist das Modell die Applikation. Wenn das Modell die Anwendung repräsentiert, dann
stellen die Sichten (Views) und Controller-Klassen das
Benutzerinterface dar. Dabei ist das Benutzerinterface konsequent in
Eingabe- und Ausgabekomponenten aufgeteilt. Der Controller ist die
Eingabekomponente. Sie liefert konkrete Informationen an das Modell. Die
Sicht dazu ist die Ausgabekomponente, welche die Informationen aus dem Modell
wiedergibt. Dabei sollte das Modell vollkommen unabhängig von seinen externen
Repräsentationen dieser Informationen sein. Dies ist ein extrem wichtiger
Punkt, da es die Wiederverwendung von Code betrifft. Es muß
möglich sein, die Eingabedaten und Ausgabeformate zu ändern, ohne
dabei das Modell anpassen zu müssen. Das Modell soll dabei nur mit reinen
Informationen arbeiten.
Anforderungen zur Konvertierung der Eingabedaten haben nichts im Modell zu
suchen. Wenn solche Konvertierungen notwendig werden, dann sind diese
vom Controller auszuführen. Ebenso übernimmt das Modell keine
Verantwortung, wie die Daten zum Schluß dargestellt werden. Das Konzept
von MVC erreicht so eine klare Schichtentrennung zwischen den Anwendungsdaten
(fachlicher Bereich), den Sichten auf die Anwendungsdaten
(Präsentationsbereich) und den Benutzerschnittstellen (Interaktionsbereich).
Das Hauptziel von MVC ist, die Verarbeitung eines Problembereiches von
seiner Präsentation zu trennen. Ein weiterer Aspekt stellt die
Steuerung der Interaktion mit dem Benutzer dar, welche die thematischen Zusammenhänge
eines Problembereiches darstellt.
Die Idee der strikten Trennung der drei Komponenten ist, daß das Gesamtsystem
hierdurch sehr flexibel gegenüber jeglichen Änderungensanforderungen
wird - egal, ob diese sich auf die fachliche Verarbeitung, die Präsentation
anwendungsbezogener Daten oder die Benutzerführung jeglicher Entwicklungsplattformen
beziehen. Eine derartige Flexibilität ist nur über diese
Schichtentrennung gewahrt.
Zusätzlich ermöglicht es dem Entwickler, durch die komplette
Isolierung der Geschäftsmodelle (business model), sich konkret seinem
eigentlichen Problembereich zu widmen. Da die unterschiedlichen
Komponenten im MW vollkommen unabhängig voneinander sind, lassen sich
diese in verschiedenen anderen Kontexten wiederverwenden. Um eine besonders
hohe Wartbarkeit eines Systems zu erreichen muß die MVGArchitektur
so einfach wie möglich aufgebaut sein. Denn mit steigender
Komplexität verringert sich auch die Wartbarkeit des Systems entsprechend.
Grundsätzlich besteht die Architektur deshalb nur aus drei Klassen:
dem Modell (model), seiner Sicht (view) und dem Controller (controller).
Die Kontrollflüsse im MVC
Beim MVC-Paradigma werden die Sichten und Objektmodelle (Models) durch
den Aufbau konkreter Protokollklassen für Benachrichtigungszwecke entkoppelt.
Das betreffende Protokoll ist dabei in entsprechenden Controller-Klassen
implementiert. Die Aufgabe eines ViewObjektes ist es, sicherzustellen,
daß seine Darstellung den konsistenten Zustand der
Objektmodelle wiedergeben. Das Modell benachrichtigt dabei alle von ihm
abhängigen Sichten, wenn sich seine Daten im Modell ändern. Hieran
sieht man gut, daß die MVC-Architektur rein nachrichtenorientiert
arbeitet. Daraufhin erhält eine Sicht die Möglichkeit, mit Hilfe
verschiedener Objektmethoden einen konsistenten Zustand herzustellen.
Der Ansatz erlaubt es, dabei mehrere Sichten an ein Objektmodell zu
hängen. Alle Sichten können, unabhängig vom Modell,
jederzeit ausgetauscht werden. Die verschiedenen Interaktionsdiagramme
beschreiben am besten die unterschiedlichen dynamischen Verhaltensweisen
innerhalb der MVC-Architektur. Hierunter finden sich die Anmeldung
und Initialisierung, der Änderungsablauf und zum Schluß die Abmeldung
und Freigabe. Die komplette MVC-Architektur ist von seinem Objektmodell
(Model) abhängig, damit es seine Aufgaben wahrnehmen kann.
Die einzige Aufgabe des Modells besteht darin, den aktuellen Status allen
interessierten Sichten und Controller zur Verfügung zu stellen.
Da das Modell jedoch vollkommen entkoppelt implementiert ist, hat es
keine Kenntnis davon, daß noch Views und Controller existieren, die
an dessen Daten interessiert sind. Deshalb geht man so vor, daß sich
alle Objekte beim Modell anmelden müssen, wenn sie von Änderungen im
Modell benachrichtigt werden wollen. Die Implementierungsweise wird in der
Literatur als Observer-Entwurfsmuster bezeichnet. Die Klassenverantwortungen
werden innerhalb der Sicht aufgebaut. So meldet sich die View beim entsprechenden
Modell an. Diese Initialisierung wird wie üblich im Hauptprogramm
implementiert. Dabei ist für jede Sicht, die im Programm geöffnet werden
soll, eine Sicht und ein Controller zu initialisieren. Nun die einzelnen
Schritte hierzu:
Als erstes wird das Modellobjekt erstellt, wobei dessen interne Datenstruktur
direkt initialisiert wird. Danach wird die View-Instanz erzeugt. Diese Instanz
erhält eine direkte Referenz auf das Objektmodell und wird als Parameter
übergeben. Mit Hilfe der addObserver()-Methode meldet sich die View
beim Objektmodell an. Die Sicht erstellt daraufhin eine Instanz der Klasse
Controller. Diese verfügt über eine initialize()-Methode,
der per Parameterübergabe eine Referenz auf das Modell und seiner
Sicht übergeben wird. Genau wie die Anmeldung bei der View
durchzuführen war, meldet sich jetzt der Controller mittels der
addObserver()-Methode beim Objektmodell an. Über die initialize()-Methode
können konkret Ressourcen reserviert öder angelegt werden, die
die View und der Controller dann verwenden. Nachdem dieser Initialisierungsvorgang
komplett abgeschlossen ist, kann die Applikation auf Benutzereingaben
reagieren und diese entsprechend weiterverarbeiten. Im folgendem wird der
Kommunikationsfluß, der dem MVC-Szenario zu Grunde liegt, beschrieben.
Das Interaktionsdiagramm zeigt hierzu konkret, wie die verschiedenen
Objekte (Modell,View,Controller) miteinander kommunizieren. Zusätzlich
kann das Objektmodell über die notifyObservers()-Methode alle angemeldeten
Sichten und Controller über Änderungen informieren. Hierzu geht
das Modell alle bei ihm angemeldeten Views und Controller durch und ruft die
update-Methode auf. Bild 1 zeigt, wie Änderungen im Objektmodell
durch Benutzereingaben den Änderungsablauf auslösen. Hierbei akzeptiert
die Applikation zuerst ein Dialogereignis eines Benutzers und ruft eine
Callback-Funktion im entsprechenden Controller auf. Dort wird die Eingabe
interpretiert und aktiviert den entsprechenden Dienst, der vom Model angeboten
wird. Das Modell führt den angeforderten Dienst aus und ändert dabei
seinen internen Zustand. Dies veranlaßt das Modell, alle bei ihm angemeldeten
Sichten und Controller durch Aufruf ihrer zugehörigen update-Methoden
mittels notifyObservers über die Änderung zu benachrichtigen. Dabei
holt sich jede Sicht die geänderten Daten vom Modell und aktualisiert
seine Bildschirmpräsentation. Jeder beim Modell angemeldete
Controller kann bestimmte Benutzerfunktionen ein- oder ausschalten, wenn
sich der Modellzustand geändert hat. Beispielsweise kann eine
Zustandsänderung des Modells den Menüpunkt zum Speichern der Daten deaktivieren.
Ist der Aktualisierungsvorgang abgeschlossen, wird die Callback-Funktion
verlassen und die Applikation kann wieder auf Benutzeraktionen
reagieren. Manchmal kommen auch Situationen vor, in denen die verschiedenen
Abhängigkeitsbeziehungen innerhalb eines MVC-Szenarios wieder
aufgelöst werden müssen, beispielsweise, wenn eine Sicht vom
Benutzer explizit geschlossen wird. Die folgenden Schritte skizzieren die
auszuführenden Operationen. Über eine CallbackFunktion im
Controller wird das Ereignis der Sicht interpretiert. Daraufhin wird die
release()-Methode des Controllers der zugehörigen Sicht
aufgerufen. Die Abmeldung von View und Modell geschieht mit dem
Aufruf der Methode deleteObserver() des Modells. Genauso meldet sich der
Controller beim Modell via der release-Methode() ab, die von der Sicht
aufgerufen wird.
Die Basisklassen im MVC
Der Hauptmechanismus im gesamten MVC wird über die Observable-Klasse
und die ObserverSchnittstelle erreicht. Denn hierüber läuft
die Anmeldung beziehungsweise Abmeldung einzelner Komponenten und
gleichzeitig die Benachrichtigung über Änderungen im Modell. Die
MVC-Anwendung nutzt die Observable-Klasse zur Implementierung des Modells,
während die Observer-Schnittstelle innerhalb der Sichten implementiert
ist. Hingegen benötigt die Controller-Klasse keine Hilfsklassen, um
Benutzereingaben zu verarbeiten. Die Sichten und Modelle erben dann von diesen
Klassen und helfen so automatisch, ein Benachrichtigungssystem
einzurichten. Jede Ansicht wird somit sofort in Kenntnis gesetzt, sobald sich
Daten im Modell geändert haben. Mittels der notifyObservers()-Methode
werden alle angemeldeten Observer-Objekte benachrichtigt. Diese rufen
dann ihrerseits ihre update()-Methode auf, um ihre
Bildschirmdarstellungen
zu aktualisieren, wobei die aktuellen Daten aus dem Modell geholt werden.
Dieser Entwurf wird in der Literatur als Observer-Entwurfsmuster
beschrieben. Dabei nimmt das Modell die Rolle des Subjekts ein. Für die
Registrierung der Beobachter-Objekte ist daher ein Klassenprotokoll
realisiert.
Somit muß jede Methode die eine Änderung im Modell vornehmen kann,
dafür Sorge tragen, daß auch die notifyObservers()-Methode
gerufen wird. Die Klasse KundenModell ist dabei von der Klasse
java.util.Observable abgeleitet, welche diesen Change-Dependend-Mechanismus
implementiert. Alle Methoden im Modell, die ihren Zustand ändern,
rufen die Methode setChanged() auf, die sich wiederum der Methode notifyObservers()
der Klasse java.util.Observable bedient. Die Methode notifyObservers()
iteriert über alle registrierten Observer-Objekte und ruft ihre
zugehörigen update()-Methoden auf. Die Observable-Klasse und das Observer-Interface
sehen dann wie in Listing 1 dargestellt aus:
Die Implementierung
Bei der Implementierung einer MVCApplikation sollte zuerst immer das
Modell implementiert werden. Erst danach sollten die verschiedenen Controller
für die verwendeten GUI-Komponenten folgen. Zum Schluß sind
dann die entsprechenden Sichten noch zu entwerfen. Das Objektmodell unseres
Beispiels enthält die Basisdaten von Mitgliedern, welche vom Modell zu
verwalten sind. Wir haben es sehr einfach gehalten und nur Nachname und
Vorname berücksichtigt (siehe Bild 6). Innerhalb der Sicht können Sie
per Eingabefeld neue Mitglieder einfügen oder bereits bestehende wieder
löschen. Die einzelnen Einträge werden dabei in einer ListBox
dargestellt (siehe Bild 7). Wenn Sie dabei mehrere Ansichten öffnen,
werden Sie sehen, daß, sobald Sie Einträge verändern, diese
sofort in allen aktiven Sichten dargestellt werden. Dies liegt daran, daß
alle Sichten sich auf ein und dasselbe Modell beziehen. Hieran kann man die
Arbeitsweise und den Vorteil von MW gut nachvollziehen.
Das Modell
Jede Applikation verfügt über eine gewisse
Funktionalität, um die Kerndaten aus der fachlichen Sicht zu verwalten.
Diese sind im MW so implementiert, daß man unabhängig von Ein- und
Ausgabesichten ist. Die Entwicklung jeder Applikation wird mit der
Erstellung der Modell-Komponente begonnen, welche die Anwendungsdaten und die
Funktionalitäten der Applikationen unabhängig von ihrer
Repräsentation kapselt. In den Modell-Komponenten werden
Methoden (holeInhalt, setzelnhalt) bereitgestellt, die einen Zugriff auf die
darzustellenden Daten ermöglichen. Diese Daten werden über sogenannte
Dienstmethoden verändert. Damit die Benutzerschnittstelle diese verwenden
kann, sind sie dort öffentlich deklariert. Da die Klasse KundenModell die
Klasse Observable erweitert, sind Objekte vom Typ KundenModell in der Lage,
eine Liste von registrierten Observer-Objekten zu verwalten. Dies wird als
Callback bezeichnet. Interessierte Observer-Objekte (views) registrieren
ihr Interesse, um von Anderungen von Modellinhalten benachrichtigt zu werden.
Sie melden sich durch Aufruf der addObserver()Methode innerhalb der
Observable Klasse an, welche eine Referenz zum Modell besitzt. Sobald
sich Modelleinträge geändert haben, benachrichtigt das Modell
alle registrierten Ansichten (views) darüber und übergibt dabei die
eigene Identität und den neuen Wert dieser lnstanzvariable an jedes
Ansichtsobjekt (view). In Listing 2 ist die Klasse KundenModell
abgebildet. Die Aufgabe der Modelle besteht lediglich darin, dessen Daten zu
verwalten. Dabei fungiert der Controller als reine Vermittlerklasse
zwischen Modell und Sicht. Relativ schnell kommt hierbei die Frage auf, wie
eine Ansicht mit dem Modell sinnvoll verbunden werden kann. Denn verschiedene
Komponenten einer Ansicht mit einem einzigen Modell zu verwenden, wäre zu
aufwendig. Am leichtesten funktioniert es, wenn für jede Komponente in der
Grafik-Bibliothek eine abstrakte Controllerklasse (Controller) existiert.
Die Ansicht
Der Konstruktor hat die Aufgabe, die Darstellung aufzubauen.
Daher ist die initialize()-Methode für die Ansicht am bedeutendsten.
Diese erledigt das Anmelden der Ansichten beim Modell und stellt dabei die
Beziehungen zum Controller her. Die Methode baut den
Abhängigkeitsmechanismus für den Fall auf, daß andere
Komponenten sich ändern. Nach erfolgreicher Initialisierung der Sichten
und Controller kann die Sicht am Bildschirm dargestellt werden. Das Abmelden
der Sichten beim Modell mit dem dazugehörigen Controller erfolgt dann
über die Methode release(). Die einzelnen Referenzen für die
betreffenden Modelle und Controller werden in zwei Instanzvariablen
gehalten. Für jede Ansicht ist eine zeichneListe-Methode() anzulegen,
die die Modelldaten auf den Bildschirm bringt. Dabei holt sich die Methode
die darzustellenden Daten direkt vom Modell. Hierbei kann die
Implementierung auf unterschiedlichen Plattformen anders ausfallen, da andere
Funktionen notwendig sind. Bei der Benachrichtigung über eine
Änderung wird in der Sicht eine update()-Methode aufgerufen. Diese
Methode ist in der Observer-Schnittstelle deklariert und daher von jeder
Ansichtsklasse aus zu implementieren. Diese update()-Methode wird bei allen
registrierten Ansichtobjekten aufgerufen, sobald das Modellobjekt die
notifyObservers()-Methode aus der Observable-Klasse ruft. Die
einfachste Aktualisierung der Ansicht kann über den Aufruf der
zeichneListe()Methode erzielt werden. Dabei werden alle Daten vom Modell
geholt und diese dann dargestellt. Es wird jedoch nicht berücksichtigt,
welche Daten sich überhaupt geändert haben. Die Art der Aktualisierung
ist bei komplexen Sichten und Modellen allerdings ineffizient. Für
eine Optimierung gibt es verschiedene Vorgehensweisen. Beim Aufruf der
update()-Methode werden einfach weitere Informationen bezüglich der
Daten, die sich konkret geändert haben, übergeben. Dadurch
kann die Sicht entscheiden, ob überhaupt und wo eine Aktualisierung
auszuführen ist. Listing 3 stellt die ListViewKlasse im
Überblick dar.
Der Controller
Grundsätzlich betrachtet man zuerst, welche Verhaltensweisen eine
Applikation auf dem User-Interface aufweist. Genau die Reaktionsweisen
sind im Controller zu implementieren. Grundvoraussetzung dabei ist,
daß jede Benutzerinteraktion ein Ereignis im darunterliegenden
System auslöst. Die einzelnen Ereignisse werden vom Controller mittels
entsprechender
Callback-Funktionen abgefangen und entsprechend interpretiert. Dies hat dann
den Aufruf betreffender Methoden im Objektmodell zur Folge. Innerhalb
der Controller-Klasse sind verschiedene Instanzvariablen (kundenModell,
listView) zu deklarieren.
Der Aufbau der Beziehung vom Controller zur Sicht und dem Modell über
Instanzvariablen (kundenModell, listView), geschieht genauso wie im Modell
mittels der initialize()-Methode. Diese ermöglichen der Controllerklasse
einen Zugriff auf das Modell und die Ansicht, um etwaige Methode dort aufrufen
zu können. So kann die setzeInhalt()-Methode gerufen werden, um Daten
dem Modell übergeben zu können. Genauso verwenden die Ansichtsobjekte
die addObserver()-Methode, um sich als Observer-Objekt zu registrieren.
Das Gegenteil erfolgt für die Freigabe und Abmeldung des Controllers
über die release()-Methode. Die Initialisierung der
Ereignisbehandlung fällt dabei auf jeder Plattform anders aus. Diese kann
in der Ansicht oder im Controller erfolgen. Die Views übernehmen im MVC
keine Verantwortung der Ereignisbehandlung, da dies allein dem Controller
vorbehalten ist. Die Sicht ist ausschließlich für die Darstellung
zuständig. Jede Frameansicht kann auch beendet werden. Dieses Ereignis
ist vom Controller abzufangen und entsprechend zu verarbeiten (WindowClosing).
Die in Listing 4 aufgeführte ListController-Klasse regelt die
beschriebenen Ereignisse.
Die Vorteile von MVC
Nun haben wir gesehen, daß die Implementierung von MVC gar nicht
so schwer ist. Folgende Vorzüge können helfen, Ihre Projekte
wesentlich flexibler zu realisieren:
(1) Mehrere Darstellungen des Modells Durch die strikte Trennung des Modells
von den Komponenten der Benutzeroberfläche ist es möglich,
mehrere Darstellungen des gleichen Modells auf unterschiedliche Weise
abzubilden. Die Sichten können auch zur Laufzeit jederzeit dynamisch
geöffnet und wieder geschlossen werden.
(2) Konsistente Darstellung
Eine Änderung der Daten im Modell bewirkt eine Benachrichtigung aller bei
ihm angemeldeten Views und Controller. Somit kann gewährleistet
werden, daß die Views und Controller sich jederzeit in einem
konsistenten Zustand zum Modell befinden.
(3) Klare Aufgabenverteilung der Objekte
Durch die strikte Aufgabenteilung von Modell, View und Controller wird eine
erhöhte Wiederverwendbarkeit der einzelnen Komponenten in anderen Kontexten
ermöglicht. Das System kann zudem sehr flexibel auf Änderungen
der Anforderung an die Applikation reagieren.
(4) Austauschbare Views und Controller
Durch die lose Kopplung der Views und Controller vom Modell in der MVC-Architektur,
ist es jederzeit möglich, die View- und Controller-Objekte eines
Modells auszutauschen. Komponenten der Benutzeroberfläche können
sogar zur Laufzeit geändert werden.
Fazit
Grundsätzlich kann man sagen, daß sich der Mehraufwand für
die Implementierung einer richtigen MVC-Architektur lohnt. Schon bei
kleineren bis mittelgroßen Anwendungen sollten Sie wegen der enormen
Vorteile nicht darauf verzichten. Dies gilt besonders für
Anwendungen, die immer wieder zu modifizieren sind. Unverständlicherweise
wurde das Konzept nur teilweise in das Grafiktoolset „Swing"
übernommen, obwohl es sich leicht realisieren läßt. Denkt
man das Konzept von MVC mal konsequent zu Ende, treten die Schwächen
innerhalb von Swing hervor. Die Grundbedingung, daß ein Modell
vollkommen unabhängig von der gewählten Darstellung sein muß,
ist dort nicht haltbar, da Swing vom konkreten GUI-Element abhängige,
untereinander inkompatible Modelle verwendet. Zudem
repräsentiert ein GUIElement in der Regel nur einen Aspekt eines komplexen
Objektes. Zum Beispiel eines Kunden mit Adresse, Kontonummer und so
weiter. Somit zerstükkelt Swing den engeren Zusammenhang der
Objekte jedoch in Modelle für jeden einzelnen Aspekt. Trotzdem wird Swing
immer mehr in Projekten verwendet. Eine Anpassung tut hier Not.