Modell-Ansicht-Bediener
(Model-View-Controller)
von Andreas Krutscher,
WI6443
[ Seminarübersicht]
... [Architekturmuster] ... [Literatur]
... [ > ]
Name
Modell-Ansicht-Bediener (Model-View-Controller)
Das Modell-Ansicht-Bediener Architekturmuster (MVC) teilt eine interaktive
Applikation in drei Komponenten. Das Modell enthält die Kernfunktionalität
und die Daten. Ansichten stellen Informationen für den Benutzer dar.
Bediener verarbeiten Benutzereingaben. Ansichten und Bediener bilden zusammen
die Schnittstelle für den Benutzer. Eine Benachrichtigungs-Strategie
stellt die Konsistenz zwischen der Benutzungsschnittstelle und dem Modell
sicher.
Auch bekannt als
Ähnlichkeiten finden sich zum Beobachter-Muster von Erich Gamma e.a.
Beispiel
Betrachten wir ein Informationssystem für politische Wahlen. Ein solches
System bietet eine Tabelle, in der Daten eingegeben werden können
und verschiedene Arten von Tabellen und Grafiken, die das aktuelle Wahlergebnis
darstellen. Der Benutzer kann in das System über eine graphische Schnittstelle
eingreifen. Alle dargestellten Informationen müssen Änderungen
des Wahlergebnisses sofort wiedergeben.
Es soll möglich sein, neue Arten der Präsentation wie zum Beispiel
eine graphische Darstellung der Aufteilung der Sitze im Parlament ohne
große Eingriffe in das System hinzuzufügen. Das System soll
auf verschiedene Plattformen mit verschiedenen "Look & feel"-Standards
portierbar sein.
Kontext
Interaktive Applikationen mit einer flexiblen Mensch-Computer Schnittstelle.
Problemstellung
Benutzerschnittstellen sind bezüglich Änderungen am System besonders
empfindlich. Wenn die Funktionalität einer Applikation erweitert wurde,
muß z.B. auch das Hauptmenü um erweitert werden, um die neue
Funktion nutzen zu können. Verschiedene Benutzer wollen vielleicht
sogar verschiedene Schnittstellen verwenden, und auf verschiedenen Plattformen
sieht die Applikation unterschiedlich aus. Sogar das Installieren einer
neuen Version des benutzen Fenstersystems kann Codeänderungen nach
sich ziehen.
Ein System mit dieser geforderten Flexibilität zu bauen ist teuer
und fehleranfällig, wenn die Benutzerschnittstelle mit dem Funktionalen
Kern der Software fest verwoben ist.
Die folgenden Kräfte beeinflussen eine Lösung dieses Problems:
-
Die selbe Information wird auf verschiedene Arten in verschiedenen Fenstern
dargestellt, z.B. in einer Balken- oder einer Tortengraphik.
-
Die Anzeige und das Verhalten der Applikation muß Änderungen
an den Daten sofort wiederspiegeln.
-
Änderungen an der Benutzerschnittstelle sollen einfach und sogar zur
Laufzeit möglich sein.
-
Die Unterstützung verschiedener "Look & Feel"-Standards oder das
Portieren der Benutzerschnittstelle auf eine andere Plattform soll die
Kernfunktionalität der Applikation nicht berühren.
Lösung
Ein Model-View-Controller (MCV) wurde das erste mal in der Smalltalk-80
Programmierumgebung eingeführt. Ein MCV teilt eine Applikation in
drei Bereiche: Verarbeitung, Ausgabe und Eingabe.
Die Modell-Komponente kapselt die Kern-Daten und -Funktionalität.
Sie ist völlig unabhängig von speziellen Ausgabe-Repräsentationen
oder Eingabeverhalten.
Ansicht-Komponenten geben Informationen an den Benutzer aus.
Eine Ansicht bezieht seine Daten von dem Modell. An ein Modell können
viele Ansicht-Komponenten gebunden sein.
Jede Ansicht hat eine zugehörige Bediener-Komponente. Bediener-Komponenten
erhalten Eingaben, gewöhnlicherweise über Ereignisse die Daten
bezüglich Mausbewegungen, Mausereignissen oder Tastatureingaben enthalten.
Solche Ereignisse wandelt die Bediener-Komponente dann in Anfragen an die
Ansicht oder das Modell um. Der Benutzer arbeitet mit dem System ausschließlich
über Bediener-Komponenten.
Die Trennung des Modells von Ansichten und Bediener-Komponenten erlaubt
mehrere Ansichten von dem selben Modell. Wenn der Benutzer die Daten des
Modells über eine Bediener-Komponente manipuliert, dann sollen alle
anderen Ansichten und Bediener-Komponenten die Änderungen sofort wiedergeben.
Dazu muß das Modell alle angemeldeten Ansichten darüber benachrichtigen,
wenn sich seine Daten geändert haben. Die Ansichten beziehen bei Erhalt
der Benachrichtigung die Daten neu vom Modell und aktualisieren ihre Ausgabe.
Dieser Änderungs-Fortpflanzungs Mechanismus wird im Darstellung-Abstraktion-Kontrolle
(PAC) Muster beschrieben.
Struktur
Modell
Wie bereits festgestellt, stellt die Modell-Komponente den funktionalen
Kern der Applikation dar. Sie kapselt geeignete Daten und bietet Prozeduren,
die auf den Daten applikationsspezifische Aufgaben ausführen. Bediener-Komponenten
rufen diese Prozeduren in gemäß Benutzervorgabe auf. Das Modell
bietet außerdem Funktionen, die von den Ansicht-Komponenten aufgerufen
werden, um Daten zum Anzeigen abzurufen.
Für den Änderungs-Fortpflanzungs Mechanismus wird im Modell
eine Registrierung der abhängigen Ansichten und Bediener benötigt.
Alle Ansichten und ebenso einige Bediener registrieren ihren Bedarf auf
Benachrichtigung. Änderungen an den Daten des Modells lösen dann
die Benachrichtigung aller registrierten Komponenten aus. Diese Registrierung
ist die einzige Verbindung zwischen dem Modell und den Ansichten und Bediener-Komponenten.
Modell |
Mitarbeiter
Ansicht
Bediener
|
Verantwortlichkeit
Bildet den funktionalen Kern der Applikation
Registriert abhängige Ansichten und Bediener-Komponenten
Benachrichtigt abhängige Komponenten über Datenänderungen
|
Ansicht
Jede Ansicht definiert eine Update-Prozedur, die im Zuge einer Benachrichtigung
durch das Modell aufgerufen wird. Innerhalb dieser Prozedur wird eine Ansicht
seine dargestellten Daten neu vom Modell beziehen und neu darstellen.
Bei der Instantiierung einer Ansicht-Klasse wird dem Konstruktor eine
Referenz auf ein Modell mitgegeben, bei dem sich das neue Ansicht-Objekt
bereits während der Initialisierung anmeldet. Jedes Ansicht-Objekt
kann zusätzlich genau eine Bediener-Komponente erzeugen. Bei dessen
Initialisierung wird ebenfalls eine Referenz auf das Modell mitgegeben,
damit sich auch der Bediener beim Modell registrieren kann. Weiterhin übergibt
das Ansicht-Objekt dem Bediener eine Referenz auf sich selbst. Dadurch
kann der Benutzer über das Bediener-Objekt die Art der Darstellung
in der Ansicht verändern. Änderungen wie z.B. das Scrollen eines
Ansicht-Bereiches betreffen nicht das Modell. Deswegen ist es sinnvoll,
daß das Bediener-Objekt direkt auf das Ansicht-Objekt Zugriff hat.
Ansicht |
Mitarbeiter
Bediener
Modell
|
Verantwortlichkeit
Erzeugt und initialisiert seine zugehörige Bediener-Komponente
Präsentiert Informationen für den Benutzer
Implementiert eine Update-Prozedur
Bezieht Daten vom Modell
|
Bediener
In welcher Weise das Bediener-Objekt seine Ereignisse erhält hängt
im wesentlichen vom Betriebssystem und der Programmierumgebung ab. Aus
Vereinfachungsgründen wird angenommen, daß die Bediener-Klasse
Ereignisbehandlungsroutinen für alle relevanten Ereignisse definiert.
Wenn das Verhalten des Bedienerobjektes vom Zustand des Modells abhängt,
muß es sich beim Modell registrieren und - wie eine Ansicht - eine
Update-Prozedur anbieten. So kann es bei einer Änderung des Zustandes
des Modell sein eigenes Erscheinungsbild den neuen Bedürfnissen anpassen.
Das ist beispielsweise sinnvoll, wenn bei bestimmten Zuständen Menüeinträge
gesperrt werden sollen.
Bediener |
Mitarbeiter
Ansicht
Modell
|
Verantwortlichkeit
Nimmt Benutzereingaben in Form von Ereignissen entgegen
Transformiert Ereignisse in Serviceaufrufe des Modells oder in Anfragen
an die Ansicht
Implementiert eine Update-Prozedur, wenn nötig.
|
Objektorientierte Implementierung
Eine objektorientierte Implementierung des MVC wird für jede Komponente
eine eigene Klasse definieren. Die Ansicht- und Bedienerklasse werden ein
gemeinsames Elternobjekt besitzen, daß die Update-Prozedur definiert.
Dies ist in folgendem Diagramm dargestellt.
In unserem Beispielsystem hält das Modell die kumulierten Stimmen
für jede politische Partei und erlaubt Ansicht-Objekten, diese Zahlen
abzufragen. Es veröffentlicht weiterhin Prozeduren zum Manipulieren
der Daten an die Bedienerobjekte.
Interaktionen
Die folgenden Szenarien betreffen das dynamische Verhalten eines Modell-Ansicht-Bediener
Systems. Aus Vereinfachungsgründen ist immer nur ein Ansicht-Bediener
Paar dargestellt.
Szenario I: Auslösen des Benachrichtigungs-Mechanismus
Der Bediener nimmt Benutzereingaben in seiner Ereignisbehandlungsroutine
entgegen, interpretiert das Ereignis und ruft den entsprechenden Dienst
des Modells auf. Das Modell führt diese Dienst aus, was unter Umständen
zu Veränderungen der internen Daten führt. Ist das der Fall,
benachrichtigt das Modell alle registrierten Ansichten und Bediener indem
es deren aktualisiere()-Prozeduren aufruft. Jede benachrichtigte
Ansicht ruft die Daten vom Modell ab und aktualisiert seine Darstellung
auf dem Bildschirm. Bediener rufen die Daten erneut ab, um ihre Bedienelemente
dem neuen Datenzustand anzupassen. Schlußendlich erhält der
auslösende Bediener die Kontrolle zurück und beendet die Ereignisbehandlung.
Szenario II: Initialisierung des MVC
Als erstes wird das Modell instantiiert, welches selbst seine internen
Daten initialisiert. Als nächstes wird eine Ansicht erzeugt. Bei seiner
Initialisierung wird ihr eine Referenz auf das Modell mitgegeben. Die Initialisierung
kann aber auch schon mit Hilfe des Konstruktors bei der Instantiierung
geschehen. Nach der Initialisierung meldet sich die neue Ansicht beim Modell
an. Die Initialisierung wird fortgesetzt indem die neue Ansicht eine Bediener-Komponente
erzeugt. Dem Bediener werden bei der Initialisierung Referenzen auf das
Modell und auf die Ansicht mitgegeben. Bei der Initialisierung registriert
sich auch der Bediener bei dem Modell. Nach der Initialisierung startet
die Applikation ihre eigene Ereignisbehandlung.
Implementierung
Die Schritte 1 bis 6 sind fundamental wichtig, wenn man ein MVC erstellen
möchte. Alle weiteren Schritte beschreiben zusätzliche Überlegungen,
die das System noch flexibler machen.
1. Trenne Mensch-Computer Interaktionen von der Kern-Funktionalität
Analysiere die Applikations-Domäne und trenne die Kern-Funktionalität
vom Ein- und Ausgabeverhalten heraus. Entwerfe die Modell-Komponente und
kapsele in ihr die Daten und die Funktionalitäten, die mit ihnen verbunden
sind. Weiterhin werden Funktionen benötigt, um auf die Daten zuzugreifen
und um die Funktionalitäten zu nutzen. Mache diese Funktionen in einer
Schnittstelle verfügbar.
class Modell {
Vector stimmen;
Vector Parteien;
public Modell(Vector parteiNamen) { Parteien
= parteiNamen };
void loescheStimmen();
void setzeStimme(String partei, long stimmen);
// ...wird fortgesetzt
}
2. Implementiere den Änderungs-Fortpflanzungs Mechanismus
Die Modell-Klasse muß um eine Registrierung erweitert werden, in
der die Referenzen auf die beobachtenden Objekte gehalten werden. Es müssen
Prozeduren zur Verfügung gestellt werden, es Ansichten und Bedienern
erlauben sich an- und abzumelden. Die benachrichtige()-Prozedur des Modells
benachrichtigt alle angemeldeten Beobachter. Alle Prozeduren, die den internen
Zustand des Modells verändern müssen nach Durchführung der
Änderungen die benachrichtige()-Prozedur aufrufen.
Wie eine Implementierung dessen aussehen kann zeigt folgender Beispielcode.
class Beobachter {
public virtual void aktualisiere() { }
// Standardmäßig keine Aktion
}
class Modell {
// ...Fortsetzung
Vector registrierung;
public void anmelden(Beobachter b) {
registrierung.addElement(b);
}
public void abmelden(Beobachter b) {
registrierung.removeElement(b);
}
protected void benachrichtige() {
for(int i = 0; i <
registrierung.elementCount; i++) {
Beobachter b = registrierung.elementAt(i);
b.aktualisiere();
}
};
}
3. Entwerfe und implementiere die Ansichten
Jetzt muß das Erscheinungsbild der Ansichten entworfen werden. Jede
Ansicht kann z.B. über eine draw() Prozedur verfügen,
die die benötigten Daten vom Modell abruft und auf dem Bildschirm
ausgibt. Die Bildschirmausgabe hängt hauptsächlich von der Benutzerschnittstelle
und der Systemumgebung und nicht zuletzt von dem Informationsbedürfnis
des Benutzers ab.
Alle verschiedene Ansicht-Klassen werden von Beobachter abgeleitet
und die aktualisiere() Prozedur mit einer passenden Implementierung
überschreiben. Die einfachste Art der Implementierung ist, innerhalb
von aktualisiere() die Prozedur draw() aufzurufen. Für
komplexe Systeme kann diese simple Implementierung jedoch ineffizient sein.
In solchen Fällen muß man andere Überlegungen anstellen
und andere Wege beschreiten. Eine Möglichkeit ist, Ansichten bei der
Benachrichtigung einen Parameter mit zu übergeben, der Auskunft darüber
gibt, was sich innerhalb der Daten verändert hat. Damit kann eine
Ansicht entscheiden, ob eine Aktualisierung überhaupt notwendig ist.
Unabhängig davon braucht jede Ansicht-Klasse eine Initialisierung,
innerhalb derer sich das Objekt beim Modell anmeldet.
class Ansicht extends Beobachter {
Modell meinModell;
Bediener meinBediener;
public Ansicht(Modell m) {
meinModell = m;
meinBediener = Null;
meinModell.anmelden(this);
}
void aktualisiere() { this.draw(); }
void draw();
Modell gibModell() { return meinModell; }
Bediener gibBediener() { return meinBediener;
}
void finalize() { meinModell.abmelden(this);
}
}
4. Entwerfe und implementiere die Bediener
Für jede Ansicht innerhalb der Applikation muß daß Verhalten
des Systems in Bezug auf Benutzereingaben spezifiziert werden. Wir setzen
dabei voraus, daß das zugrunde liegende Betriebssystem die nötigen
Geschehnisse der Applikation in Form von Events mitteilt. Eine Bediener-Komponente
muß nun nur noch diese Ereignisse entgegennehmen und interpretieren.
Innerhalb der Initialisierung eines Bedieners wird es an das Modell
und an eine Ansicht gebunden. Wie danach die Ereignisbehandlung gestartet
wird hängt wiederum von der Systemumgebung und der verwendeten Programmiersprache
ab.
Der beispielhafte Quelltext zeigt eine abstrakte Bediener-Klasse, bei
der die bearbeiteEreignis()-Prozedur in Unterklassen überschrieben
werden muß.
class Bediener extends Beobachter {
Modell meinModell;
Ansicht meineAnsicht;
public void Bediener(Ansicht a) {
meineAnsicht = a;
meinModell = meineAnsicht.gibModell();
meinModell.anmelden(this);
}
void bearbeiteEreignis(Event e) { }
void aktualisiere() { }
void finalize() { meinModell.abmelden(this);
}
}
Weiterhin sind natürlich noch Methoden nötig, die die Daten des
Modells modifizieren. Der Aufruf solcher Methoden erfolgt aus der bearbeiteEreignis(Event
e)-Prozedur heraus.
5. Entwerfe und implementiere die Ansicht-Bediener Beziehung
Eine Ansicht erzeugt ihren zugehörigen Bediener typischerweise während
der Initialisierung. Es ist allerdings besser, die Erzeugung des Bedieners
in eine Fabrikmethode, z.B. Bediener erzeugeBediener() zu kapseln.
So haben Unterklassen der Ansicht-Klasse die Möglichkeit, gezielt
die Erzeugung des Standard-Bedieners gegen einen anderen Bediener auszutauschen
indem sie die Fabrikmethode überschreiben.
6. Implementiere die Initialisierung des MVC
Die Initialisierung des Systems geschieht in der in Szenario
II dargestellten Reihenfolge. Nach der Initialisierung wird das System
in eine Ereignisbehandlungs-Routine übergehen.
Die Initialisierung des oben dargestellten Beispielcodes könnte
wie folgt aussehen. Diese Programmzeilen sind Teil eines übergeordneten
Programmabschnittes:
Vector parteiNamen = new Vector();
parteiNamen.addElement("CDU/CSU");
parteiNamen.addElement("SPD");
parteiNamen.addElement("FDP");
parteiNamen.addElement("B90/Grüne");
parteiNamen.addElement("Sonstige");
Modell m = new Modell(parteiNamen);
Ansicht a1 = new TabellenAnsicht(m);
Ansicht a2 = new SaeulenAnsicht(m);
// Start der Ereignisbehandlung
7. Dynamische Ansichten-Erzeugung
Soll die Applikation das dynamische Öffnen und Schließen von
Ansichten erlauben, ist es sinnvoll, eine weitere Komponente zum Verwalten
der Ansichten zu erstellen.
8. Austauschbare Bediener
Die Trennung des Bedieners von der Ansicht ermöglicht es, für
eine Ansicht verschiedene Arten von Bedienern zur Verfügung zu stellen.
Das ermöglicht es uns, Benutzern mit verschiedenen Fähigkeiten
verschiedene Bediener-Komponenten anzubieten. Ein erfahrener Benutzer wird
eine umfangreichere Benutzerschnittstelle verwenden wollen als ein Benutzer,
der zum ersten mal mit dem System arbeitet. Somit ist es auch möglich,
bestimmten Benutzergruppen ausschließlich lesenden Zugriff auf die
Daten zu gewähren.
9. Infrastruktur für hierarchische Ansichten und Bediener
Eine MVC-Komponente wie z.B. eine Ansicht kann selbst wiederum aus einer
vielzahl von wiederverwendbaren Ansichten oder Bedienern bestehen. Dies
sind im allgemeinen Elemente, die häufig verwendet werden, also Pushbuttons,
Edit-Felder, Menüs usw. Die Benutzerschnittstelle wird erstellt, indem
vorhandene Komponenten verwendet und zusammengefügt werden. Dieses
Zusammensetzen wird im Kompositum-Muster (E. Gamma, e.a.) näher betrachtet.
10. Weitere Trennung von Systemspezifischen Details
Ein solches komplexes MVC-System zu entwickeln kann unter Umständen
sehr teuer sein und es wird gewünscht, das System auf mehreren Plattformen
anzubieten. In einem solchen Fall ist es sinnvoll, weitere Abstraktionsschritte
durchzuführen und die MVC-System-Details von den plattformabhängigen
zu trennen indem sie in getrennten Klassen definiert werden. Diese müssen
dann über eine definierte plattformneutrale Schnittstelle miteinander
kommunizieren.
Varianten
Dokument-Ansicht
Diese Variante entspannt die Trennung zwischen Ansicht und Bediener. In
vielen GUI Plattformen ist die Bildschirmausgabe stark mit der Ereignisbehandlung
verwoben. Man kann die Ansicht- mit der Bedienerkomponente verschmelzen.
Dann muß man allerdings hinnehmen, daß die Bedienung der Ansicht
nicht mehr austauschbar ist. Dabei ist es nach wie vor möglich, mehrere
Ansichten eines Dokumentes zu erzeugen.
Bekannte Verwendung
Smalltalk ist mit seinem Benutzer-Schnittstellen Framework das am
besten bekannte Beispiel für eine Implementierung des MVC-Musters.
MFC. In der Visual C++ Entwicklungsumgebung ist die Dokument-Ansicht
Variante des MVC-Musters integriert.
ET++ implementiert ebenfalls die Dokument-Ansicht Variante
des MVC-Musters. Eine typische ET++-basierte Applikation implementiert
seine eigene Dokument-Klasse und eine zugehörige Ansicht-Klasse.
Konsequenzen
Vorteile:
-
Mehrere Ansichten des selben Modells. Das MVC-Muster trennt das
Modell strikt von der Benutzerschnittstelle. Das ermöglicht es, mehrere
Ansichten der selben Daten in verschiedenen Formen zu erzeugen.
-
Synchronisierte Ansichten. Das Änderungs-Fortpflanzungs Konzept
stellt sicher, daß alle Ansichten von Änderungen an den Daten
Kenntnis bekommen.
-
Austauschbare Ansichten und Bediener. Die konzeptionelle Trennung
des MVC erlaubt es, die Ansichten und Bediener-Objekte eines Modells auszutauschen.
Benutzerschnittstellen können sogar zur Laufzeit ausgetauscht werden.
-
Austauschbarkeit des "Look & Feel"'s. Eine Portierung des Systems
betrifft nur die Ansichten und Bediener, nicht jedoch die Kernfunktionalität.
-
Framework-fähig. Es ist möglich, komplette Anwendungs-Frameworks
auf dieses Muster zu stützen.
Konsequenzen:
-
Erhöhte Komplexität. Die Komplexität wird durch eine
vielzahl einzelner Klassen erhöht.
-
Gefahr von exzessiv vielen Aktualisierungen. Wenn ein Benutzereingriff
in zahlreichen Aktualisierungen mündet ist zu überlegen, wie
unnötige Aktualisierungen vermieden werden können. Eventuell
sind nicht alle Ansichten an jeder Art von Änderung interessiert,
da sie nur einen Ausschnitt der Daten repräsentiert.
-
Private Beziehungen zwischen Ansicht und Bediener. Eine Ansicht
und ein Bediener können in einer Beziehung stehen die verhindert,
daß die Komponenten individuell wiederverwendet werden können.
-
Enge Kopplung der Ansichten und Bediener an das Modell. Beobachter
unternehmen direkte Methodenaufrufe beim Modell. Falls mal die Schnittstelle
des Modells verändert werden sollte, so sind von dieser Änderung
ebenso alle Ansicht- und Bedienerklassen betroffen.
-
Ineffizienter Datenzugriff in Ansichten. Abhängig von der Schnittstelle
des Modells muß eine Ansicht womöglich mehrere Funktionsaufrufe
beim Modell unternehmen, um alle gewünschten Daten zu erhalten.
-
Änderungen an Ansichten und Bedienern beim Portieren unvermeidbar.
Beide Komponenten beinhalten auch Code, der plattformunabhängig ist.
Eine Portierung erfordert also die Trennung von plattformabhängigem
von -unabhängigem Code selbst in den Ansichten- und Bedienerklassen.
-
MVC mit modernen GUI-Tools schwierig. Das Benutzen eines visuellen
Konstruktionstools kann die Verwendung des MVC-Musters ausschließen.
Es ist schwer, die Ergebnisse eines solchen Tools an das MVC-Schema anzupassen
ohne nach der Anpassung auf die weitere Nutzung des Tools zu verzichten.
Verwandte Muster
Das Darstellung-Abstraktion-Kontrolle (PAC) Muster
wählt einen anderen Ansatz, um die Benutzerschnittstelle vom funktionalen
Kern der Applikation zu trennen. [BMR96]
[ Seminarübersicht]
... [Architekturmuster] ... [Literatur]
... [ > ]