Entwurfsmuster EVA MVC Beobachter Algorithmustest neues Formular sichere Eingabe Zeitmessung
Pfad: Startseite / Fächer / Informatik / Delphi / Entwurfsmuster / Beobachter
Autor: mk
30.09.2006 16:42:00
13297
Beobachter-Muster

Definition

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.

Benutzung der vereinfachten Delphi-Variante des Beobachter-Musters

Klassendiagramm zu Observer0

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;

uTSubjekt.zip

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.

Sequenzdiagramm zur Veranschaulichung der Interaktionen

Sequenzdiagramm zu Observer0 sequenz0.violet

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).

Bemerkungen

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.

Allgemeines Beobachtermuster

Folgendes Klassendiagramm zeigt die Struktur des Beobachtermusters, wie es Gamma, Helm, Johnson, Vlissides in ihrem Buch auf Seite 289 vorstellen:

Beobachtermuster

Erste Realisierung in Delphi

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;

  .....

GUI zu Test ohne Interface, mit Polymorphie Beobachter2Test.zip

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;
  ....

Verbesserung

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;
....

GUI zu BeobachterTest BeobachterTest.zip

Praktische Benutzung des Observer-Patterns am Beispiel

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.

MVCObserver.zip

Gedanken zur Realisierung des Beobachter-Musters

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.

GUI zu Observer0 observer0.zip

Quellcode des Modells

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.

Details zum 'Einhängen'

...

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;

...

Praktische Benutzung des Observer-Patterns

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.

Die Basis-Klasse 'TSubjekt'

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.

Das UML-Diagramm

UML-Diagramm zur Vererbung

Die abgeleitete Klasse 'TBus'

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.

Die Realisierung

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.

GUI zu Observer1 observer1.zip

Aufgabe

Verstehe das in http://www.ordix.de/ORDIXNews/1_2002/java_3.html angeführte Mailverteiler-Problem und implementiere es in Delphi.

Links

Valid XHTML 1.0! lokal