Model-View-Controller (MVC)

Datum: 20.03.2000
(Applet: 12.10.2012)
Version: 1.01
Author: Marc Votteler
 

Swing macht es vor und alle machen es nach ?

  1. Was steckt hinter dem MVC-Konzept ?
  2. Swing macht es vor
  3. MVC in eigenen Softwareprojekten



1. Was steckt hinter dem MVC-Konzept ?

Bei dem MVC-Konzept handelt es sich um das Prinzip die Datenhaltung (Model),die Datenrepräsentation (View) und die Datenmanipulation (Controller) von einander zu trennen. Der Vorteil bei dieser Vorgehensweise ist, dass es zu einem späteren Zeitpunkt sehr leicht möglich ist, die vorhanden Daten neu zu visualisieren in dem man einfach nur den View austauscht. Dabei braucht man sich keine Gedanken um die datenverarbeitenden bzw. datenmanipulierenden Teile des Programms machen, da diese ja entkoppelt sind. Bei dem Controller und dem Model verhält sich das natürlich vergleichbar.




Abbildung 1.1

Es ist auch möglich mehrere Views zu verwenden die auf das gleiche Model zugreifen. Zum Beispiel kann man sich den Spannungsverlauf einer Stromquelle in einer Kurve darstellen lassen. Als weiteres besteht nun die Möglichkeit einen Punkt auf der X-Achse (Zeit) anzuklicken um den entsprechenden Wert der Y-Achse (Spannung) als Fließkommazahl in einem weiteren Feld angezeigt zu bekommen.

Die einzelnen Komponenten lassen sich folgendermaßen beschreiben:

Model (Datenhaltung, Anwendung)
Das Model kann man sich als das Datenmodell vorstellen, der den aktuellen Zustand und das Verhalten des gesamten Systems repräsentiert.

View (Visualisierung, Darstellung)
Der View hat die Aufgabe die Daten des Models auf irgend eine Art darzustellen (im Normalfall visualisieren). Wichtig ist hierbei, dass die Darstellung der Daten die einzige Aufgabe ist, die der View erfüllen muss.

Controller (Datenmanipulation, Steuerung)
Der Controller setzt die eingehenden Anforderungen (z.B. Eingaben von der Tastatur oder Messwerte eines Sensors) in Signale um, die das Model dazu veranlassen, die Daten entsprechen zu verändern.

Da bei den meisten Programmen die Interaktion mit dem Benutzer über einen Bildschirm für die Visualisierung und einer Tastatur oder einer Maus für die Eingabe realisiert wird, besteht hier ein sehr enger Zusammenhang zwischen dem View und dem Controller. Diese Verknüpfung wird im allgemeinen als Benutzerschnittstelle oder UI (User Interface) bzw. GUI (Graphical User Interface) bezeichnet.



Abbildung 1.2

[top]

2. Swing macht es vor

Mit Swing hat Sun Microsystems ein sehr komfortables Framework für die Erstellung von graphischen Oberflächen für Java geschaffen (wer schon mal mit MFC arbeiten mussten, weiß was ich meine). Bei der Architektur von Swing hat man sehr stark auf die Entkopplung der Daten von ihrer Darstellung bzw. Manipulation geachtet (MVC -Konzept). Oftmals finden sich der View und der Controller in einer Komponente wieder (vgl. Abbildung 1.2). In diesem Fall reagiert das entsprechende Objekt auf die Aktionen des Benutzers und Visualisiert seinen entsprechenden Status. Als Beispiel soll hier der JSlider dienen (Applet 2.1). Er reagiert auf das drücken der linken Maustaste und verändert seinen Wert (Model) wenn die Maus horizontal bewegt wird so lange, bis die linke Maustaste wieder losgelassen wird. Gleichzeitig visualisiert er die Veränderung des Models durch das Anpassen der Position des "Griffes" (Wenn einer weiß wie der Griff vom Slider heißt -> mail) entsprechend der Datenänderung.



Applet 2.1

Bei diesem einfachen Applet habe ich es mir gespart den Sourcecode zur Verfügung zu stellen.

[top]

3. MVC in eignen Softwareprojekten

Der Aufwand seine Softwarearchitektur nach dem MVC-Konzept zu gestalten und dann in Quellcode umzusetzen ist anfänglich größer, als wenn man einfach alles mit einander "verstrickt" wie es gerade erforderlich ist. Vor allem weil man in vielen Fällen noch Adapterklassen braucht um das Model an den View bzw. den Controller an das Model anzupassen. Aber dazu später mehr.

Die Vorteile die man dadurch hat sprechen jedoch für sich:

  • Das Erstellen von mehreren Views bzw. das später Hinzufügen von Views wird erheblich erleichtert.
  • Durch die klare und durchgängige Struktur ist die Software leichter zu warten (auch nach längerer Zeit findet man sich in dem Code noch zurecht).
  • Die Wiederverwendbarkeit der einzelnen Klassen wird erhöht.
  • Der View bzw. der Controller können auch dynamisch zur Laufzeit getauscht werden.
  • Durch die Separation braucht nach einer Änderung in einer der Komponenten nur diese getestet werden (das Interface muss natürlich unverändert bleiben)


Wie kann man nun dieses Konzept in der eigenen Softwareentwicklung einfließen lassen ?

Am einfachsten lässt sich das an einem Beispiel erklären. Realisiert werden soll ein Applet, dass ein Feld und zwei Buttons für die Eingabe hat. Wird auf den einen Button geklickt, so wird ein Wert inkrementiert, wird auf den anderen Button geklickt, so wird dieser Wert dekrementiert. Mit Hilfe des Feldes kann direkt ein Wert vorgegeben werden. Als weiteres sollen die Daten visuell an Hand von zwei Balken (ein horizontaler und ein vertikaler) und einem Feld dargestellt werde. In diesem Beispiel stellen die Buttons bzw. das Eingabefeld die Controller und die Balken bzw. das Ausgabefeld die Views dar.



Applet 3.1 (src / doc)

Das Model lässt sich hier glaube ich am schnellsten abhandeln. Es ist absolut Anwendungsspezifisch und das einzige worauf es an kommt, ist dass es unabhängig vom View und vom Controller gehalten werden muss. Um die Verbindung zu den Controllern bzw. den Views dann herzustellen, wird das Eventmodells von Java verwendet.

In unserem Fall verwenden wir den <ChangeListener>. Um ChangeEvents verschicken bzw. empfangen zu können muss einiges vorbereitet werden. Die Klasse die <ChangeEvents> empfangen soll, muss das Interface <ChangeListener> implementieren (UML: Realisierung). Dieses Interface bewirkt, das ein Objekt dieser Klasse unter anderem eben als <ChangeListener> angesprochen werden kann. Und als weiteres ist sichergestellt, dass es eine Methode <stateChanged(ChangeEvent)> gibt, die von der eventsendenden Klasse aufgerufen wird wenn sich etwas geändert hat.
Die Klasse, die <ChangeEvents> senden möchte, sollte Methoden zur Verfügung stellen, über die man <ChangeListener> registrieren (z.B. <addChangeListener(ChangeListener)>) als auch entfernen (z.B. <removeChangeListener(ChangeListener)>) kann. Innerhalb dieser Klasse gibt es ein Objekt vom Typ <EventListenerList>. Wie der Name es schon andeutet, handelt es sich hierbei um einen Klasse die <EventListener> aufnehmen kann, also eine Art "EventListenerArray". In dieser <EventListenerList> werden alle <Listener> die sich registrieren (<addChangeListener(ChangeListener)>) abgelegt. Löst ein Anwender jetzt eine Aktion aus, so wird die Liste der <ChangeListener> in der <EventListenerList> durchgearbeitet und bei jedem Objekt das da drin ist die Methode <stateChanged(ChangeEvent)> aufgerufen.

Dies ist meiner Meinung nach die eleganteste Variante die Controller mit dem Model zu verbinden (trotz aller Kapselung müssen sie ja dann doch irgend wie mit einander kommunizieren). Von einem JButton zum Beispiel kennt man diese Vorgehensweise bereits. Man registriert einen <ActionListener> (knopf.addActionListener(listener)), der die Benutzeraktion (Knopf wurde gedrückt) auffängt.
In unserem Fall werden die Controller durch komplexere Klasse repräsentiert. Sie bestehen selber aus einem Label und einem Button bzw. einem Textfeld. Diese Klassen ihrerseits können nun die <Event>s verschicken (<ChangeEvent>). Diese <Event>s werden bei einer Benutzeraktion (Button geklickt oder Zahl eingegeben) an die registrierten <ChangeListener> versendet (in unserem Fall Objekte der KLasse <Model>). Nachdem der <Event> vom <Model> ausgewertet wurde (was hat den <Event> ausgelöst?) wird die entsprechende Aktion ausgeführt.

In dem folgenden Klassendiagramm (Klassendiagramm?) ist dieses dargestellt :



Abbildung 3.1

Damit die Klasse Model nicht alle verschieden Controller-Klassen vom Typ her kennen muss (das würde bedeuten, dass bei jeder neuen Controller-Klasse der Sourcecode der Model-Klasse angepasst werden müsste), implementieren alle Controller-Klassen das Interface <Controller>. Dieses Interface definiert die Methoden <getCommand()> und <getValue()>, damit die Model-Klasse bei einem empfangenen <ChangeEvent> nachfragen kann, was es mit seinen Daten tun soll. Als weiteres werden noch die Methoden <addChangeListener(ChangeListener)> und <removeChangeListener(ChangeListener)> angegeben damit sich die <Listener> (hier die Model-Klasse) registrieren bzw. eine vorhanden Registrierung wieder aufheben können.

Das angepasste Klassendiagramm (Klassendiagramm?) sieht dann so aus:



Abbildung 3.2

An der Stelle im Programm, wo die hier verwendeten Klassen instantiiert werden, muss das Model-Objekt als <ChangeListener> bei den Controller-Objekten registriert werden (<controllerObj.addChangeListener(modelObj)>). Wird nun vom Anwender ein Knopf gedrückt, so feuert (... das heißt nun mal so) das Controller-Objekt einen <ChangeEvent> an alle registrierten <ChangeListener> (in unserem Fall nur das Model-Objekt). Unser Model-Objekt kann nun auswerten wo der <ChangeEvent> herkam und reagiert durch entsprechende Maßnahmen darauf, d.h. es fragt das auslösende Controller-Objekt was es wie tun soll (<getCommand()> und <getValue()>). Wird in das Eingabefeld (im <ControllerField>) etwas eingegeben und CR gedrückt so verhält sich das Ganze äquivalent.

Jetzt müssen unsere Daten natürlich auch irgendwo erscheinen (am Besten im View, sonst ist irgend etwas schief gelaufen). Das Model muss nun den Views bescheid sagen, dass sich seine Daten geändert haben. Die Verbindung zwischen dem Model und den Views wird wieder über eine Event-Kommunikation mit dem <ChangeListener> realisiert.

Auch hier soll das folgende Klassendiagramm (Klassendiagramm?) das verdeutlichen:



Abbildung 3.3

Nun sind es die Views die das Interface <ChangeListener> implementieren müssen um vom Model benachrichtigt werden zu können. Diese müssen sich dann auch wieder bei dem eventverschickenden Objekt (Model-Objekt) registrieren (<modelObj.addChangeListener(viewObj)>). Ändern sich nun die Daten im Model-Objekt, benachrichtigt es alle registrierten <ChangeListener> (Views), dass sich etwas geändert hat. Diese fragen dann das Model-Objekt nach dem aktuellen Wert (<modelObj.getValue()>) und stellen den neuen Wert da.

Schauen wir das Ganze noch mal in dem kompletten Klassendiagramm (Klassendiagramm?) an:



Abbildung 3.4

Hier noch einmal der komplette Ablauf:

  • der Anwender drückt einen Button oder gibt einen Wert in das Textfeld ein
  • das entsprechende Controller-Objekt sendet einen <ChangeEvent> an das registrierte Model-Objekt
  • das Model-Objekt fragt das Controller-Objekt welche Operation ausgeführt werden soll (<controllerObj.getCommand()>) und mit welchem Wert (<controllerObj.getValue()>)
  • das Model-Objekt sendet einen <ChangeEvent> an alle registrierten View-Objekte
  • die View-Objekte fragen das Model-Objekt nach dem neuen Wert (<modelObj.getValue()>) und zeigen diesen an



Vom Prinzip her war das schon alles. Einen Punkt gibt es noch den ich hier erwähnen möchte. In manchen Fällen passt der Controller von den Daten die er zur Verfügung stellt nicht zu dem Model. Oder der View kann mit den Daten vom Model nichts anfangen. In unserem Beispiel ist das auch vorgekommen. Das Abfrageergebnis von einem <JTextField> ist vom Typ <String> aber unser Model kennt nur <int>. Hier konnte man mit <Integer.parseInt(textFeldObj.getText())> das Ergebnis nach <int> wandeln. Aber es gibt Programme, da ist das nicht so einfach möglich.
In solchen Fällen verwendet man Adapter. Ein Adapter ist lediglich eine weitere Klasse die zwischen Controller und Model bzw. zwischen Model und View gestellt wird und nichts weiter zu tun hat als die eingehenden Daten in ein Format zu übersetzen, dass von dem Anderen verstanden werden kann.

Abschließend sei erwähnt, dass man die Sache in jedem Fall realistisch sehen muss. Für so eine Anwendung wie ich sie hier als Beispiel verwendet habe, braucht man sicherlich keine großen Gedanken an irgend welche Design-Patterns verschwenden. Hier wäre es auf jeden Fall schneller gegangen, wenn man einfach alles so zusammengestöpselt hätte wie es gerade am einfachsten geht. Aber es handelt sich ja wie gesagt um ein Beispiel. Bei größeren Programmen ist es aber meiner Meinung nach nahezu unumgänglich, sich an solche Konzepte zu halten, da ansonsten die Software schnell unübersichtlich wird und später schwer zu warten ist (hinzufügen oder ändern von Views / Controllern).


Klassendiagramm! - Begriffsdefinition aus der UML:

  1. Assoziation (dargestellt durch eine einfache Linie, die die beteiligten Klassen verbindet) - eine Assoziation beschreibt die Tatsache, dass Objekte der einen Art mit Objekten der anderen Art in einer strukturellen Beziehung stehen. Bei einer Verkettung von Objekten, können beide Enden einer Assoziation auch auf die selbe Klasse zeigen.
  2. Aggregation (dargestellt durch eine nicht gefüllte Raute an einem Ende einer Assoziation) - eine Aggregation ist eine Spezialform der Assoziation die eine Ganzes/Teil-Beziehung repräsentiert. Diese Beziehung wird auch oft als eine "Hat-ein"-Beziehung bezeichnet. Eine Aggregation ist eine rein konzeptionelle Beziehung, die keine Aussage über eine Abhängigkeit zwischen der Lebensdauer der beteiligten Klassen zulässt. Die Aggregation wird oft mit der Komposition verwechselt (früher war alles anders ...).
  3. Komposition (dargestellt durch eine gefüllte Raute an einem Ende einer Assoziation) - eine Komposition ist eine Art der Aggregation die eine stärkere Bindung repräsentiert. Die Teile (Klassen am entfernten Ende der Raute) bei einer Komposition hängen direkt von dem Ganzen (Klasse bei der Raute) ab, d.h. sie gehören diesem einen Ganzen und können dieses auch nicht überdauern.
  4. Realisierung (dargestellt durch ein nicht gefülltes Dreieck an einem Ende einer gestrichelten Linie) - bei einer Realisierung wird meistens durch ein Interface (an dem Ende mit dem Dreieck) ein Vertrag spezifiziert (eben die Schnittstelle), den die andere Klasse zu erfüllen hat (die Methoden implementieren bzw. implementieren lassen).



Dokument Version 1.01 vom 20.03.2000 (Applets zuletzt erstellt mit JDK 1.6.0_35 am 12.10.2012)
Bei Anmerkungen oder Fragen zu diesem Dokument bitte Mail an mich

[top]