HSG |
|
Definiere eine 1-zu-n-Abhängigkeit zwischen Objekten, sodass die Änderung des Zustands eines Objekts dazu führt,
dass alle abhängigen Objekte benachrichtigt und automatisch aktualisiert werden.
aus Erich Gamma, Richard Helm, Ralph Johnson, John Vlissides, Entwurfsmuster, Seite 287
Es kann häufig die Situation auftreten, dass eine Änderung im 'Model' mehrere 'Beobachter' interessiert. Das 'Model' soll die Beobachter über seine Zustandsänderung informieren. Die Schwierigkeit dabei ist, dass durch die angestrebte völlige Entkopplung das 'Model' 'Views' und 'Controls' zur Entwurfszeit nicht kennen darf. Erst während der Laufzeit melden sich interessierte 'Beobachter' beim 'Model' an. Sie abonnieren gewissermaßen eine Benachrichtigung über eine Änderung. Dieses 'Abonnement' können sie natürlich auch wieder rückgängig machen.
type TEreignis = procedure of object; TSubjekt = class(tObject) protected OnChange : array of TEreignis; // interne Liste procedure benachrichtige; // Aufruf aller Routinen der Liste public procedure meldeAn(routine: TEreignis); // neuer 'Event-Handler' in Liste procedure meldeAb(routine: TEreignis); // 'Event-Handler' aus Liste entfernen end;
Obiges Klassendiagramm spiegelt die Situation wider, dass ein (1) konkretesSubjekt (Bezeichnung stammt von der 'Gang of Four') von mehreren (n) konkretenBeobachtern beobachtet wird. Die Klasse TSubjekt wird dabei nicht angetastet, sondern nur als 'Stammvater' für die Klasse 'TkonkretesSubjekt' benutzt. Sehr häufig wird das das Model in einer MVC-Struktur sein. Selbstverständlich empfiehlt es sich, einen jeweils passenden Namen für diese Klasse zu wählen. Zur Benutzung/Vererbung wird man etwa eine Unit uTSubjekt z.B. aus einem Verzeichnis muster/observer0 dem Projekt hinzufügen. Die ererbte Methode Benachrichtige wird an den relevanten Stellen der Klasse TkonkretesSubjekt eingefügt. Die konkretenBeobachter (auch hier wird man passende Namen wählen) kennen das konkreteSubjekt und können mit Hilfe der Methoden meldeAn und meldeAb ihre Routinen, die z.B. Aktualisiere oder Update oder so ähnlich heißen können als Ereignisbehandlungen an- bzw. abmelden. Diese Routinen müssen parameterlos sein.
Im Sequenzdiagramm werden Objekte durch gestrichelte senkrechte Linien dargestellt. Die Zeit wächst von oben nach unten. Nachrichten werden als waagrechte Pfeile zwischen den Objekt-Linien gezeichnet. Auf den Pfeilen wird die Nachricht notiert. Die breiten Rechtecke auf den Lebenslinien symbolisieren den Steuerungsfokus (welches Objekt hat gerade die Programmkontrolle). Obiges Diagramm wurde mit Violet erstellt, das eine gewisse Darstellung erzwingt.
In obigem Diagramm sieht man, wie zunächst die Objekte Beobachter1 und Beobachter2 ihre jeweilge Routine update anmelden. Dann löst Controller1 eine Zustandsänderung bei Model aus. Das wiederum bewirkt den internen Aufruf der Methode Benachrichtige. Dieser Aufruf kann als Senden einer Nachricht zu sich selbst aufgefasst werden. Als Folge wird Beobachter1 die Nachricht update geschickt (eigentlich wird bei uns update direkt aufgerufen). Vermutlich hat update eine Nachfrage GetZustand nach dem Zustand von Model zur Folge. Das Gleiche passiert dann mit Beobachter2 (Benachrichtige geht die ganze Liste durch).
Manche Programmiersprachen wie z.B. Java lassen prozedurale Variablen nicht zu. Damit wird es unmöglich, eine Liste von Ereignisbehandlungsroutinen zu führen. Stattdessen könnte die Liste ganz konkret die Referenzen auf die Beobachter- Objekte enthalten. Diesen Beobachtern kann man nur eine Nachricht schicken, wenn man den Namen der Nachricht, z.B, Aktualisiere kennt. Diese Überlegungen führen zum allgemeinen Beobachtermuster. Dieses Muster enthält eine Klasse TAbstrakterBeobachter. Ein konkreter Beobachter muss nun von einer Klasse sein, die TAbstrakterBeobachter erweitert. Gleichzeitig will man aber z.B. für Views eine andere Funktionalität erben. Das führt zum Problem der Mehrfachvererbung, die in Delphi und Java nicht möglich ist. Lösungen wie z.B. die von Shaun Parry beschriebene Observer-Pattern-Implementierung verwenden ebenfalls prozedurale Variablen und erscheinen mir aufwändiger.
Folgendes Klassendiagramm zeigt die Struktur des Beobachtermusters, wie es Gamma, Helm, Johnson, Vlissides in ihrem Buch auf Seite 289 vorstellen:
unit uBeobachter2; interface uses classes; // für TList type TBeobachter = class(TObject) procedure aktualisiere; virtual; abstract; end; TSubjekt = class(TObject) protected beobachterliste : TList; // interne Liste von Zeigern procedure benachrichtige; // Aufruf aller Routinen der Liste public constructor create; destructor destroy; procedure meldeAn(beobachter : TBeobachter); procedure meldeAb(beobachter : TBeobachter); end; .....
Der folgende Quelltextauszug zeigt, wie die drei Views zu Beobachtern werden und wie die Methode Aktualisiere so (unelegant?) implementiert wird, dass sie den richtigen View auswählt.
.... type TView = class(TBeobachter) public procedure aktualisiere; override; end; TForm1 = class(TForm) ..... private model : TModel; view1,view2,view3 : TView; public { Public-Deklarationen } end; ..... procedure TView.aktualisiere; begin if self = form1.view1 then form1.label1.Caption := IntToStr(Form1.model.GetZahl); if self = form1.view2 then form1.label2.Caption := IntToStr(Form1.model.GetZahl); if self = form1.view3 then form1.label3.Caption := IntToStr(Form1.model.GetZahl); end; ....
Die Klasse TBeobachter enthält nur die abstrakte Methode Aktualisiere. Ein konkreter Beobachter muss aber von dieser Klasse erben, damit sich die Funktionalität erhält. Häufig möchte man aber von einer weiteren Klasse erben, die z.B. viele View-Eigenschaften bereitstellt. Das ist das Problem der Mehrfachvererbung, die in Delphi nicht möglich ist. Trotzdem lässt sich das Observer-Pattern verbessern, indem man TBeobachter zu einem Interface macht, denn in diesem eingeschränkten Sinn ist Mehrfachvererbung möglich.
.... type TBeobachter = interface procedure aktualisiere; end; ....
Labels eignen sich als einfache Views sehr gut. Man kann nun gleichzeitig die Funktionlität von TLabel erben und die Vorgaben von TBeobachter implementieren.
....
type
TMyLabel = class(TLabel,TBeobachter)
public
procedure aktualisiere;
end;
....
procedure TMyLabel.aktualisiere;
begin
Caption := IntToStr(Form1.model.GetZahl);
end;
....
Die Methode Aktualisiere wird wunderbar einfach. Es soll aber nicht verschwiegen werden, dass Labels vom Typ TMyLabel natürlich 'von Hand' eingebunden und positioniert werden müssen.
.... procedure TForm1.FormCreate(Sender: TObject); begin model := TModel.Create; view1 := TMyLabel.Create(Form1); with view1 do begin Parent := gbView; Caption := '---'; Left := 230; Top := 40; end; view2 := TMyLabel.Create(Form1); with view2 do begin Parent := gbView; Caption := '---'; Left := 230; Top := 80; end; view3 := TMyLabel.Create(Form1); with view3 do begin Parent := gbView; Caption := '---'; Left := 230; Top := 120; end; end; ....
In MVCPolling.zip ist eine einfache MVC-Demonstration realisiert, die von Polling auf Observer umgebaut werden soll.
Veränderungen in der Modell-Unit:
unit uTModel; interface uses uBeobachter; // Unit einbinden type TModel = class(TSubjekt) // erbt von TSubjekt private zahl : integer; public procedure SetZahl(pZahl : integer); function GetZahl : integer; end; implementation procedure TModel.SetZahl(pZahl : integer); begin zahl := pZahl; Benachrichtige; // Benachrichtige einfügen end; function TModel.GetZahl : integer; begin result := zahl; end; end.
Veränderungen in der GUI-Unit:
unit uMVCObserver; interface uses Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms, Dialogs, StdCtrls, uTModel, uBeobachter; // Unit einbinden type TMyLabel = class(TLabel,TBeobachter) // View-Typ definieren public procedure Aktualisiere; end; TForm1 = class(TForm) eEin: TEdit; bSetze: TButton; procedure FormCreate(Sender: TObject); procedure FormDestroy(Sender: TObject); procedure bSetzeClick(Sender: TObject); private model : TModel; lAus : TMyLabel; public { Public-Deklarationen } end; var Form1: TForm1; implementation {$R *.dfm} procedure TMyLabel.Aktualisiere; // neu implementieren begin Caption := IntToStr(Form1.model.GetZahl); end; procedure TForm1.FormCreate(Sender: TObject); begin model := TModel.Create; lAus := TMyLabel.Create(Form1); // View wird erzeugt lAus.Parent := Form1; lAus.Top := 100; lAus.Left := 20; lAus.Caption := '---'; model.meldeAn(lAus); // View meldet sich an end; procedure TForm1.FormDestroy(Sender: TObject); begin lAus.Free; model.Free; end; procedure TForm1.bSetzeClick(Sender: TObject); begin model.SetZahl(StrToInt(eEin.Text)); // lAus.Caption := IntToStr(model.GetZahl); // Polling end; end.
In der Sprache der 'Ereignisse' könnte man das 'Observer-Pattern' folgendermaßen beschreiben: Die Zustandsänderung im Modell-Objekt ist ein Ereignis, das bei mehreren Beobachter-Objekten eine Ereignisbehandlung auslösen soll. Die Beobachter sollen diese Ereignisbehandlungen beim Modell 'einhängen' und auch widerrufen können. Das beschriebene Verhalten kann in Delphi dadurch erreicht werden, dass das Modell eine dynamische Liste von Ereignisbehandlungen (Eventhandler) führt, die bei der interessierenden Zustandsänderung aufgerufen werden.
Das kleine Demonstrationsprogramm enthält ein Modell, das als einzigen inneren Zustand eine byte-Variable hat. Dieser Zustand kann auf drei verschiedene Arten von den 'Controls' verändert werden. Drei Labels dienen als 'Views'. Ein Update der Views kann durch 'Einhängen' der Ereignisbehandlungen in die Liste des Modells geschehen. Diese Liste enthält eigentlich Einsprungadressen, die durch einen Typecast zu anzeigbaren integer-Werten gemacht wurden. Natürlich ist diese Liste eigentlich im privaten Teil des Modells, sie wurde hier nur zu Demonstrationszwecken zugänglich gemacht.
unit mTBus0; interface type TEreignis = procedure of object; TBus = class(tObject) private // OnChange : array of TEreignis; // interne Liste inhalt : byte; public OnChange : array of TEreignis; // nur zu DEBUG-Zwecken public procedure SetInhalt (pInhalt: byte); function GetInhalt : byte; procedure meldeAn(routine: TEreignis); // neuer 'Event-Handler' in Liste procedure meldeAb(routine: TEreignis); // 'Event-Handler' aus Liste entfernen end; implementation procedure TBus.meldeAn(routine : TEreignis); var n : integer; begin n := Length(OnChange); SetLength(OnChange,n+1); OnChange[n] := routine; end; procedure TBus.meldeAb(routine : TEreignis); var i,j : integer; begin i := Low(OnChange); while i <= High(OnChange) do // High liefert -1 bei leerem Array begin if @OnChange[i] = @routine // mit '@' nur Adressen vergleichen then begin for j := i to High(OnChange)-1 do OnChange[j] := OnChange[j+1]; SetLength(OnChange,Length(Onchange)-1); end else i := i+1; end; end; procedure TBus.SetInhalt (pInhalt: byte); var i : integer; begin inhalt := pInhalt; // alle Ereignis-Behandlungs-Routinen der Liste aufrufen for i := Low(Onchange) to High(OnChange) do Onchange[i]; end; function TBus.GetInhalt : byte; begin result := inhalt; end; end.
... procedure TForm1.update1; begin l1.Caption := IntToStr(bus.GetInhalt); end; ... procedure TForm1.bMeldeAn1Click(Sender: TObject); begin bus.meldeAn(update1); updateListe; // nur zu DEBUG-Zwecken end; ...
Um obige Umsetzung des Beobacher-Musters zu benutzen, kann man nun immer die passenden Code-Zeilen in die jeweilige Klassendefinition einkopieren. Das wäre eine sehr unelegante und aufwändige Lösung. Man denke nur an den Fall, dass der bereits in vielen Klassen einkopierte Code geändert werden müsste. Jede einzelne Klasse müsste verändert werden und wehe es würde eine vergessen! Da wäre es doch viel besser, es gäbe einen Mechanismus, den vielen verschiedenen - beobachtbaren - Klassen die gleiche Funktionalität mitzugeben ohne den Code einzufügen. Dieser Mechanismus ist die Vererbung.
unit uTSubjekt;
interface
type
TEreignis = procedure of object;
TSubjekt = class(tObject)
private
OnChange : array of TEreignis; // interne Liste
protected
procedure benachrichtige; // Aufruf aller Routinen der Liste
public
procedure meldeAn(routine: TEreignis); // neuer 'Event-Handler' in Liste
procedure meldeAb(routine: TEreignis); // 'Event-Handler' aus Liste entfernen
end;
implementation
procedure TSubjekt.meldeAn(routine : TEreignis);
var
n : integer;
begin
n := Length(OnChange);
SetLength(OnChange,n+1);
OnChange[n] := routine;
end;
procedure TSubjekt.meldeAb(routine : TEreignis);
var
i,j : integer;
begin
i := Low(OnChange);
while i <= High(OnChange) do // High liefert -1 bei leerem Array
begin
if @OnChange[i] = @routine // mit '@' nur Adressen vergleichen
then
begin
for j := i to High(OnChange)-1 do OnChange[j] := OnChange[j+1];
SetLength(OnChange,Length(Onchange)-1);
end
else
i := i+1;
end;
end;
procedure TSubjekt.benachrichtige;
var
i : integer;
begin
// alle Ereignis-Behandlungs-Routinen der Liste aufrufen
for i := Low(Onchange) to High(OnChange) do Onchange[i];
end;
end.
Der Vorteil der Vererbung kommt natürlich nur zum Tragen, wenn die Klasse der Quellcode der Klasse 'TSubjekt' nur an einer Stelle steht. Das realisiert man in Delphi durch eine Einbindung der Unit 'uTSubjekt.pas' in das Projekt.
unit mTBus1; interface uses uTSubjekt; type TBus = class(tSubjekt) private inhalt : byte; public procedure SetInhalt (pInhalt: byte); function GetInhalt : byte; end; implementation procedure TBus.SetInhalt (pInhalt: byte); var i : integer; begin inhalt := pInhalt; // alle Beobachter informieren benachrichtige; end; function TBus.GetInhalt : byte; begin result := inhalt; end; end.
Das obige Testprogramm wurde dahingehend abgeändert, dass obige Klasse 'TBus' benutzt wird. Die Liste der Ereignisbehandlungs-Routinen ist nicht mehr zugänglich und wurde weggelassen. Das kann man beruhigt tun, der Anmelde- und Abmelde-Vorgang wurde ja getestet.
Verstehe das in http://www.ordix.de/ORDIXNews/1_2002/java_3.html angeführte Mailverteiler-Problem und implementiere es in Delphi.