Observer-Pattern mit Delphi XE4

Für ein Projekt benötigte ich heute eine Lösung, nach der verschiedene Objekte sich gegenseitig über Ereignisse benachrichtigen können. Randbedingung war, dass nur eines der (beiden) Objekte das jeweils andere bzw. die jeweils anderen Objekte kennt. Die klassische Situation also für das “Observer-Pattern“.

Da war ich ja schon drauf & dran, eigene Interfaces nieder zu schreiben, als mir bewusst wurde, dass Delphi dies ja bereits integriert hat. Und zwar für alles, was von TComponent ableitet – was in meinem Szenario der Fall war. Bevor ich also nun das Rad neu erfinde, nahm ich doch lieber die Bordmittel von Delphi. Und das ging erfreulich leicht.

Also nochmals die Ausgangssituation:

  • Wir haben ein Datenobjekt, welches seiner täglichen Arbeit nachgeht und von seiner Umwelt nicht mehr weiß als das, was es unmittelbar benötigt.
  • Wir haben ein anderes Objekt – den Beobachter – der wissen möchte, wann sich bei dem Datenobjekt ein bestimmter Wert ändert.
  • Es könnte einen, zwei oder hundert dieser Beobachter geben, vielleicht auch sehr unterschiedliche Beobachter. Das Datenobjekt soll diese gar nicht kennen müssen.

Das Datenobjekt ist in unserem Beispiel trivial:

type
  TMyDataObject = class(TComponent)
  private
    FData: integer;
    procedure SetData(const Value: integer);
  public
    constructor Create(AOwner: TComponent); override;
  published
    property Data: integer read FData write SetData;
  end;

Gemäß dem “Observer-Pattern” müssen wir folgende Situation schaffen:

  • Das Datenobjekt muss eine allgemeine Schnittstelle anbieten, über die sich interessierte Beobachter für eine Benachrichtigung anmelden können.
  • Das Datenobjekt muss in der Lage sein, den Versand dieser Benachrichtigungen anzustoßen.

Hierbei hat uns die RTL von Delphi zum Glück schon viel Arbeit abgenommen: Es ist lediglich eine einzige Methode, die wir im Datenobjekt implementieren – genauer gesagt: übeschreiben – müssen. Und zwar handelt es sich dabei um die Funktion “CanObserve”, die wir von TComponent erben. Diese boolsche Funktion gibt “true” zurück, wenn sie beobachtet werden möchte und “false”, wenn ihr das gerade weniger gelegen kommt. In die Deklaration unseres Datenobjektes fügen wir also folgenden Code ein:

protected
  // new method for observer-support:
  function CanObserve(const ID: integer): Boolean; override;

Im einfachsten Fall liefern wir nur “true” zurück. Der Parameter “ID” ist dem LiveBindings-System von Delphi entsprungen und erlaubt eine Steuerung, welche Arten des LiveBindings in einem speziellen Fall zugelassen würden. Für den Anfang würde ich diesen Parameter an die Seite legen, schlichtweg ignorieren und die Funktion “CanObserve” grundsätzlich “true” zurück liefern lassen.

Bleibt noch die Setter-Methode “SetData”. Diese wird ja aufgerufen, wenn der Eigenschaft “Data” ein neuer Wert zugewiesen wird. Für uns also der richtige Ort, um etwaige Beobachter zu benachrichtigen. Lediglich eine Zeile haben wir der Methode hinzu zu fügen:

procedure TWeatherData.SetTemperature(const Value: integer);
begin
  if (Value <> FTemperature) then
  begin
    FTemperature := Value;
    TLinkObservers.ControlChanged(Self);  // new line to notify observers  end;
end;

Diesen Aufruf von “.ControlChanged()” können wir so bedingungslos stehen lassen. Ob es überhaupt Observer gibt und wenn ja, wer diese konkret sind, das alles wird uns von Delphi abgenommen.

Für das Datenobjekt war es das dann auch schon. :-) Tat kaum weh und – das ist das Schöne – das Datenobjekt wird durch die neue Funktionalität nicht “verschmutzt”.

Kommen wir zum Beobachter: Hier stellt Delphi das Interface “IObserver” zu Verfügung. Dieses Interface müssen wir implementieren. Ich schlage vor, dies in einer eigenen Klasse zu machen – auf diese Weise können wir das gesamte Geraffel, welches für das Observer-Pattern benötigt wird, in einer Klasse zusammenfassen.

TObservingComponent = class(TComponent, IObserver)
private
  FObserverActive : boolean;
protected
  function GetActive: Boolean;
  procedure SetActive(Value: Boolean);
  procedure Removed;
  function GetOnObserverToggle: TObserverToggleEvent;
  procedure SetOnObserverToggle(AEvent: TObserverToggleEvent);
public
    constructor Create( AOwner : TComponent ); override;
published
  property Active : Boolean read GetActive write SetActive;
  property OnObserverToggle : TObserverToggleEvent read GetOnObserverToggle write SetOnObserverToggle;
end;

Das sieht auf den ersten Blick nach viel Zeugs aus, doch die meisten dieser Methoden können wir einfach leer lassen. Lediglich die Getter- und Setter-Methoden der Eigenschaft “Active” werden wir implementieren und dort das Feld “FObserverActive” zurückliefern oder mit einem neuen Wert belegen.

Von dieser Klasse leiten wir jetzt unseren eigentlichen Beobachter ab. Hinzu kommen noch zwei Interfaces, die dem LiveBindings-Framework von Delphi geschuldet sind. Das zweite Interface, “IControlValueObserver”, ist für uns wichtig und definiert zwei Methoden:

TMyDataDisplay = class(TObservingComponent, IMultiCastObserver, IControlValueObserver)
  public
    procedure ValueModified;
    procedure ValueUpdate;
  end;

Die Methoden “ValueModified” und “ValueUpdate” werden aufgerufen, wenn sich ein Wert unseres unter Beobachtung stehenden Datenobjektes verändert hat. Tatsächlich werden sie sogar beide Methode direkt nacheinander aufgerufen, was etwas viel des Guten ist. Implementieren müssen wir zwar beide, um dem Interface gerecht zu werden, mit Leben füllen werden wir jedoch nur eine der Methoden. Die Auswahl bleibt dem geneigten Leser überlassen, da jedoch verlässlich beide Methoden aufgerufen werden, spielt es keine Rolle, welche verwendet wird, um auf die Änderungen des Datenobjektes zu reagieren.

0
Daniel Wolf

About the Author:

Daniel Wolf ist als Software-Architekt im Miniatur Wunderland Hamburg tätig, war zuvor viele Jahre als technischer Berater tätig und spricht regelmäßig auf Konferenzen. Er betreibt das Forum "Delphi-PRAXiS". » Mehr Details zur Person
  Verwandte Blog-Einträge