Thema: Delphi Observer-Pattern

Einzelnen Beitrag anzeigen

Der_Unwissende

Registriert seit: 13. Dez 2003
Ort: Berlin
1.756 Beiträge
 
#9

Re: Observer-Pattern

  Alt 5. Sep 2006, 10:36
Wortspiel?

Deine aktuelle Frage ist das was die Pattern so interessant macht. Ein Pattern sagt dir nur, dass es eine solche Möglichkeit geben soll. Wie du diese umsetzt schreibt es nicht vor.
Es gibt in Delphi zwei Möglichkeiten, wie du das ganze machen kannst (mindestens). Beide wurden eigentlich schon von generic genannt.

Möglichkeit 1 besteht in der Verwendung eines Methodenzeigers. Einen solchen Zeiger kann man als eigenen Datentypen deklarieren und dann diesen Zeiger registrieren/deregistrieren.
Der Vorteil liegt klar darin, dass hier jede Methode registriert werden kann, die die gleiche Signatur verwendet. Dies ist aber auch gleichzeitig der Nachteil. An sich ist es der weniger OOP Weg. Die Objekt Orientierung versucht immer mit abstrakten Objekten zu arbeiten. Sicherlich ist ein Zeiger sehr sehr abstrakt, aber eben kein Objekt. Das heißt nicht, dass man den Weg nicht verwenden darf/sollte.
Trotzdem bleibt zu bedenken, dass man hier wirklich beliebigen Methoden erlaubt sich für die Benachrichtigung durch ein Ereignis zu registrieren. Das ist nicht unbedingt ein Pro für die Typsicherheit (oder Ähnliches).

Möglichkeit 2 besteht in der Verwendung des Kommandopatterns (wo du gleich siehst, dass sich Pattern super kombinieren lassen). Das Kommandopattern ist die Entsprechung eines Callbacks ohne Mehtoden-/Funktionszeiger. Hierbei wird die Verhaltensverbung in der OOP ausgenutzt. Hat eine Basisklasse A eine Method doFoo, so steht die auch allen Kinder B, C, ... und Kindeskindern (usw) zur Verfügung.
Um auch hier abstrakt zu bleiben wird als Basisklasse eine abstrakte Klasse verwendet. In Delphi entspricht dies einer Klasse mit einer abstrakten Methode. Alle Nachfahren müssen also diese Methode implementieren. Alternativ eignet sich auch ein Interface.
Der eigentliche Callback besteht jetzt darin, dass man konkrete Instanzen dieser Basisklasse hat (konkret in dem Sinne, dass die entsprechende Methode hier nicht abstrakt ist). Da man weiß, dass jeder Nachfahre der Basisklasse diese Methode implementieren muss, kann diese aufgerufen werden. Letztlich entspricht das in gewisser Weise natürlich wieder dem Prinzip der Methodenzeiger (aber implizit!).

Letzteres trifft man (imho) häufiger an, da es immer mehr Sprachen gibt, die Abstand von einer expliziten Zeigerarithmetik nehmen.

Deshalb möchte ich etwas mehr auf das 2te Beispiel eingehen:
Bei dem Observer Pattern gibt es zwei Klassen von Beteiligten. Einerseits die Observer (in beliebiger Anzahl). Dann gibt es noch ein einzelnes Objekt, das Observable. Die Observer beobachten das Observable. Soweit so klar.
Das Pattern geht hier aber etwas weniger intuitiv ran. Beobachten trifft es nicht ganz, vielmehr benachrichtigt das Observable die Observer über das Eintreten eines Ereignisses, für dass sie sich registriert haben. Nur registrierte Beobachter werden also über ein Ereignis informiert. Ein Observable kann dabei auch über mehr als ein Ereignis benachrichtigen, die Observer müssen sich dann auch für jedes Ereignis einzeln anmelden (Durchaus erwünscht, erspart unnötige Benachrichtigung von Observern die die Nachricht eh verwerfen).
Der Vorteil liegt hier klar auf der Hand, du kannst leicht etwas registrieren, was in einen Fernaufruf endet. Dein Objekt kann die Kapselung eines Vertreters sein, dessen Treiber sogar auf einer gänzlich anderen Plattform läuft (was zu hohen Kosten bei der Benachrichtigung führen kann, diese lassen sich durch die gezielte registrierung vermeiden). Das alles interessiert aber das beobachtete Objekt nicht.

Ja, werden wir nun etwas konkreter. Ich denke ein gutes Beispiel ist ein Editor. Dieses besteht aus einem Eingabefeld, dass warnehmen kann, dass jmd. etwas tippt. In Delphi könntest du also einfach von einem TMemo ausgehen. Das Ereingis onChange wird hier immer aufgerufen wenn sich der Text ändert. Ja, jetzt die Überleitung zum Observer. In Delphi hast du ja schon eine Möglichkeit dieses Ereignis zu beobachten. Du verwendest eben dieses Ereignis.
Dabei wird nichts anderes gemacht als der Methodenzeiger dieses Ereignis auf eine Methode gesetzt. Die IDE ermöglicht dir dabei auch gleich die zugehörige Methode zu erzeugen.
Jetzt sagen wir, du möchtest hier mehr als eine Sache machen, dann siehst du schnell wo das Problem an dieser Lösung liegt. Sagen wir einerseits möchtest du die Syntaxprüfen. Was du dabei machst ist erstmal egal. Du möchtest wissen ob die Syntax bei dem geänderten Text noch valide ist.
Andererseits möchtest du gerne, hm, automatische Zeilenumbrüche. Wenn die Zeile länger als 80 Zeichen ist, möchtest du das die an dieser Stelle umgebrochen wird.
Hast du nur einen Methodenzeiger ist das ganze schwer. Da du sauber arbeitest packst du die Syntaxprüfung und den Zeilenumbruch in je eine Klasse. Tritt das Ereignis ein, so wird von jeder Klasse eine Instanz in der Ereignisbehandlung verwendet um die jeweilige Aufgabe zu erfüllen. Problem nun, wie kannst du weitere Klassen hinzufügen für weitere Aufgaben? (Stichwort wäre hier Plugins). Diese sind dir nicht vorab bekannt, du kannst also nichts konkretes vorsehen. Natürlich kann es auch sein, dass du die Syntaxprüfung abschalten möchtest, da diese nur noch manuell ausgelöst werden soll.

Hier heißt die Lösung Observer-Pattern. Dieses gibt eine Lösung vor, in der sich Observer an- und abmelden können. Ob der Observer dabei ein Plugin ist oder nicht ist egal.
Dein TMemo ist hier das beobachete Objekt, das Observable. Ok, ein paar Kleinigkeiten fehlen noch, aber es wird halt das Observable (was ja klar ist).
Jetzt gilt es zu überlegen, was genau Beobachtet werden kann. Das OnChange Ereignis kannst du dabei aussen vor lassen. Wichtig ist, was sollen die Beobachter erfahren? Da sich hier der Inhalt des TMemo geändert hat und dieser ein TStrings ist, bietet es sich an diesen Inhalt zu übergeben. Wem der gehört und wo der herkommt ist der Syntaxprüfung egal. Die kann durch ein TStrings gehen und die Worte auf Validität prüfen. Analog arbeitet die Zeilenumbruchklasse.
Was du also brauchst ist eine Nachricht (nicht mit den Windowsnachrichten zu verwechseln!) die den Observern das geänderte TStrings-Objekt zur Verfügung stellt.

Dies kannst du in eine Abstrakte Basisklasse packen:
Delphi-Quellcode:
type
  TAbstractContentChangedListener = class(TObject)
    public
      OnContentChanged(const newContent : TStrings); virtual; abstract;
  end;
Dies ist deine abstrakte Basisklasse für alle Observer. Sowohl die Syntaxprüfung als auch der Zeilenumbruch müssen von dieser Klasse erben. Damit kannst du beide über die Änderung des Inhalts des Memos benachrichtigen. Den geänderten Inhalt (und damit das eigentliche Ereignis) bekommen sie als Argument.
Die Observer müssen jetzt also von dieser Klasse erben und die Methode auf ihre Weise implementieren. Wie sie dies tun ist deren Sache (Stichwort Abstraktion/Black-Box).

Was jetzt noch fehlt ist die Vervollständigung des Observables. Dieses muss ein Möglichkeit bieten, dass sich hier Observer registrieren und deregistrieren können und diese Observer auch benachrichtigen.
Das ist aber nun sehr einfach möglich. Es ist schließlich bekannt von welcher Basisklasse die Observer abstammen. Objekte von diesem Typ dürfen sich also registrieren/deregistrieren. Dazu werden sie irgendwie gespeichert (natürlich bietet sich eine Liste an, aber auch hier gibt es keine Einschränkungen).
Die eigentliche Benachrichtigung hat dann die Form, dass über die Liste iteriert wird und bei allen gespeicherten Objekten diese Methode mit dem veränderten Inhalt des TMemo aufgerufen wird.

Delphi-Quellcode:
type
  TObservableMemo = class(TMemo)
    private
      FContentChangeObserver : TObjectList; // irgendeine Möglichkeit zum Speichern
    protected
      procedure notifyContentChangeObserver;
    public
      procedure registerContentChangeObserver(const Observer : TAbstractContentChangeObserver);
      procedure deregisterContentChangeObserver(const Observer : TAbstractContentChangeObserver);
  end;
Soweit die Klasse. Was beim Registrieren / Deregistrieren gemacht wird ist klar. Letztlich hängt hier alles von der gewählten Implementierung ab. Im Fall einer TObjectList würde die Benachrichtung dann die folgende Form haben:
Delphi-Quellcode:
procedure TObservableMemo.notifyContentChangeObserver;
var i : Integer;
begin
  if self.FContentChangeObserver.Count > 0 then
  begin
    for i := 0 to self.FContentChangeObserver.Count - 1 do
    begin
      TAbstractContentChangeObserver(self.FContentChangeObserver[i]).onContentChanged(self.Lines);
    end;
  end;
end;
Ja, das war es auch schon. Die langen Namen sind dabei extra gewählt. Du kannst so auch dieses Memo noch um die Möglichkeiten erweitern über Tastendrücke oder Mausereignissse zu informieren. Ein Observable ist nicht nur auf ein Ereignis festgelegt. Ein Observer natürlich geanau so wenig. Möchtest mehr als ein Ereignis beobachten hast du aber das Problem, dass du nicht von mehr als einer Klasse erben kannst. Deshalb musst du dann an der Stelle auf Interfaces zurückgreifen und die sind unter Delphi nicht ganz so komfortabel wie in anderen Sprachen.


[ADD]
Ok, gerade lange geschrieben. Obwohl der Beitrag damit vielleicht schon überflüssig ist poste ich ihn trotzdem. Im Zweifel werden die Links trotzdem besser sein
[/ADD]
  Mit Zitat antworten Zitat