![]() |
Observer-Pattern
Hallo zusammen.
Ich habe für ein kleines Projekt das Observer-Pattern versucht möglichst wiederverwendbar und einfach umzusetzen. Ich würde gerne Eure Meinung und Verbesserungsvorschläge hören und gleichzeitig das Ganze auch alles hier zur Verfügung stellen. Der Code des Patterns ist wie folgt:
Delphi-Quellcode:
Ich habe das ganze eingesetzt, um bei Änderungen an einem Logbuch automatisch in allen möglichen Formularen automatisch benachrichtigt zu werden. Dazu habe ich im Logbuch folgendes ergänzt:
unit PatObserver;
interface uses SysUtils, Classes; type INotifyObserver = interface ['{F4A2A0D8-385E-4D38-ABF5-056ED79532BC}'] procedure ObserverNotify(Sender: TObject); end; TObserverSubject = class protected ObserverCollection: TInterfaceList; public constructor Create; destructor Destroy; override; procedure RegisterObserver(Observer: INotifyObserver); procedure UnregisterObserver(Observer: INotifyObserver); procedure NotifyObservers(); end; implementation { TObserverSubject } constructor TObserverSubject.Create; begin ObserverCollection := TInterfaceList.Create(); end; destructor TObserverSubject.Destroy; begin FreeAndNil(ObserverCollection); inherited; end; procedure TObserverSubject.RegisterObserver(Observer: INotifyObserver); begin ObserverCollection.Add(Observer); end; procedure TObserverSubject.UnregisterObserver(Observer: INotifyObserver); begin ObserverCollection.Remove(Observer); end; procedure TObserverSubject.NotifyObservers; var I: Integer; fIntf: INotifyObserver; begin for I := 0 to ObserverCollection.Count - 1 do begin if Supports(ObserverCollection.Items[I], INotifyObserver, fIntf) then begin fIntf.ObserverNotify(Self); end; end; end; end.
Delphi-Quellcode:
An den Methoden, an denen das Logbuch verändert wird, wird dann
TLogFile = class(TInterfacedObject)
(*SNIP*) public Notifier: TObserverSubject; constructor Create; destructor Destroy; override; (* SNIP *) {...} constructor TLogFile.Create; begin {...} Notifier := TObserverSubject.Create; end; destructor TLogFile.Destroy; begin {...} Notifier.Free; inherited; end;
Delphi-Quellcode:
aufgerufen.
Notifier.NotifyObservers();
Um mich im LogViewer als Observer einzutragen habe ich folgendes gemacht:
Delphi-Quellcode:
{...}
type TLogViewer = class(TForm, INotifyObserver) {...} private procedure ObserverNotify(Sender: TObject); {...} procedure TLogViewer.FormCreate(Sender: TObject); begin inherited; Logfile.Notifier.RegisterObserver(Self); end; procedure TLogViewer.FormDestroy(Sender: TObject); begin inherited; Logfile.Notifier.UnregisterObserver(Self); end; procedure TLogViewer.ObserverNotify(Sender: TObject); begin // Hier auf die Benachrichtigung reagieren end; |
AW: Observer-Pattern
Delphi-Quellcode:
Vorschläge:
TObserverSubject = class
protected ObserverCollection: TInterfaceList; public constructor Create; destructor Destroy; override; procedure RegisterObserver(Observer: INotifyObserver); procedure UnregisterObserver(Observer: INotifyObserver); procedure NotifyObservers(); end; * statt TInterfaceList IInterfaceList verwenden (spart ein FreeAndNil im Destruktor) * in den Parametern const verwenden: statt (Observer: INotifyObserver) (const Observer: INotifyObserver), so kann eine unnötige Referenzzählung verhindert werden * Generics verwenden um die Observerliste typsicher zu machen (spart das Supports(...)) |
AW: Observer-Pattern
Zitat:
|
AW: Observer-Pattern
Danke schonmal für das Feedback. Ich werde also entsprechend const-Parameter nutzen und IInterfaceList verwenden.
Was die Generics angeht: Warum genau sollte ich die hier nutzen? Ich hatte auch mit sowas angefangen, aber der Datentyp von dem, was da als Observer registriert interessiert mich ja nicht. Im Gegenteil, ich will ihn ja gar nicht einschränken. Deshalb bin ich auf Interfaces gegangen. Wo wäre der denn Vorteil der Generics hier? |
AW: Observer-Pattern
Zitat:
Delphi-Quellcode:
entfällt, wenn die ObserverCollection mit INotifyObserver statt IInterface arbeitet.
if Supports(ObserverCollection.Items[I], INotifyObserver, fIntf) then
|
AW: Observer-Pattern
Zitat:
Delphi-Quellcode:
auch durch ein einfaches
Supports
Delphi-Quellcode:
ersetzen. Somit reduziert sich die Methode auf:
as
Delphi-Quellcode:
Richtig interessant für Generics wäre aber schon ein generisches Interface á la:
procedure TObserverSubject.NotifyObservers;
var I: Integer; begin for I := 0 to ObserverCollection.Count - 1 do begin (ObserverCollection.Items[I] as INotifyObserver).ObserverNotify(Self); end; end;
Delphi-Quellcode:
Damit könnte dann eine einzige Klasse für unterschiedliche Subject-Typen mehrere ObserverNotify-Methoden in einer Klasse implementieren.INotifyObserver<T> = interface procedure ObserverNotify(Sender: T); end; Leider scheitert dies aber erstmal daran, daß man einem generischen Interface bei der "Spezialisierung" (wenn man den generischen Typ auflöst) keine eigene GUID mitgeben kann. Somit hat das Interface dann entweder keine GUID oder immer die gleiche. Damit lassen sich dann aber auch keine (sinnvollen)
Delphi-Quellcode:
und
Supports
Delphi-Quellcode:
Aufrufe mehr damit machen.
as
Arbeitet man dann aber mit generischen Interfaces ohne GUID muss zwangsläufig die ObserverCollection vom Typ TList<INotifyObserver<T>> sein. Ergänzend dazu habe ich das Subject noch ausgelagert. Damit vermeidet man zum Einen, daß die Subject-Klasse von dem ObserverSubject abgeleitet werden muss, und zum Anderen könnte man die Subject-Instanz noch austauschbar machen, ohne die Observer zu deregistrieren und wieder zu registrieren. Der ganze Code sähe dann so aus:
Delphi-Quellcode:
Ein Beispiel für eine mehrere Observer implementierende Instanz wäre z.B.:
type
INotifyObserver<T> = interface procedure ObserverNotify(Sender: T); end; TObserverSubject<T> = class private FSubject: T; protected ObserverCollection: TList<INotifyObserver<T>>; public constructor Create(ASubject: T); destructor Destroy; override; procedure RegisterObserver(Observer: INotifyObserver<T>); procedure UnregisterObserver(Observer: INotifyObserver<T>); procedure NotifyObservers(); property Subject: T read FSubject; end; constructor TObserverSubject<T>.Create(ASubject: T); begin ObserverCollection := TList<INotifyObserver<T>>.Create(); FSubject := ASubject; end; destructor TObserverSubject<T>.Destroy; begin ObserverCollection.Free; inherited; end; procedure TObserverSubject<T>.NotifyObservers; var intf: INotifyObserver<T>; begin for intf in ObserverCollection do begin intf.ObserverNotify(Subject); end; end; procedure TObserverSubject<T>.RegisterObserver(Observer: INotifyObserver<T>); begin ObserverCollection.Add(Observer); end; procedure TObserverSubject<T>.UnregisterObserver(Observer: INotifyObserver<T>); begin ObserverCollection.Remove(Observer); end;
Delphi-Quellcode:
type
TMyClient = class(TInterfacedObject, INotifyObserver<TComboBox>, INotifyObserver<TStrings>) protected procedure ObserverNotify(Sender: TComboBox); overload; procedure ObserverNotify(Sender: TStrings); overload; end; |
AW: Observer-Pattern
*Thread ausgrab*
Ich bin mit dem Pattern immer noch nicht wirklich zufrieden. Im Prinzip hätte ich gerne einen Observer, der für alle Events herhalten kann. Ich habe jetzt dazu mit der RTTI ein wenig gebastelt. Der Ansatz sieht so aus:
Delphi-Quellcode:
Es läuft aber noch nicht rund. Im Moment sieht das ganze im Aufruf so aus:
unit thObserver;
interface uses System.Generics.Collections, Rtti; type TObseverItem = class public Methods: TList<TRttiMethod>; constructor Create; destructor Destroy; override; end; TObserver = class(TDictionary<string, TObseverItem>) public procedure RegisterObserver(EventName: string; Method: TRttiMethod); procedure UnregisterObserver(EventName: string; Method: TRttiMethod); procedure Call(EventName: string; Args: array of TValue); end; implementation procedure TObserver.Call(EventName: string; Args: array of TValue); var Item: TRttiMethod; begin for Item in Items[EventName].Methods do begin Item.Invoke(Item, Args) end; end; procedure TObserver.RegisterObserver(EventName: string; Method: TRttiMethod); begin if not ContainsKey(EventName) then begin Add(EventName, TObseverItem.Create); end; Items[EventName].Methods.Add(Method); end; procedure TObserver.UnregisterObserver(EventName: string; Method: TRttiMethod); begin Items[EventName].Methods.Remove(Method); if Items[EventName].Methods.Count = 0 then begin Items[EventName].Free; Remove(EventName); end; end; { TObseverItem } constructor TObseverItem.Create; begin Methods := TList<TRttiMethod>.Create; end; destructor TObseverItem.Destroy; begin Methods.Free; inherited; end; end.
Delphi-Quellcode:
Um jetzt von außerhalb mich an das Event anzuhängen, mache ich folgendes:
type
TMyClass = class private FObserver: TObserver; public constructor Create; destructor Destroy; override; procedure RegisterOnChange(Method: TRttiMethod); procedure UnregisterOnChange(Method: TRttiMethod); end; {...} // zum auslösen, um z.B. Button1 als Parameter zu übergeben (wir gehen davon aus, dass die registrierte Funktion auch darauf passt FObserver.Call('OnChange', [TValue.From<TButton>(Button1)]);
Delphi-Quellcode:
Das ganze geht so lange gut, bis der Aufruf über das Invoke erfolgt, dann hagelt es eine AccessViolation. Ist das so, wie ich das vorhabe überhaupt möglich? (Noch schöner wäre, wenn man sich mit Include und Exclude an das Event anhängen könnte, ähnlich wie in .NET. Also dann sowas wie procedure TForm1.TestOnChange(Button: TButton); begin // ... end; {...} var Method: TRttiMethod; context: TRttiContext; begin Method := context.GetType(Self.ClassType).GetMethod('TestOnChange'); Myclass.RegisterOnAquireAchievement(Method);
Delphi-Quellcode:
, aber da weiß ich auch nicht, ob und wie das geht).
Myclass.OnChange.Include(MeinEventHandler)
|
AW: Observer-Pattern
Ja geht, das hat Stevie vor kurzem hier gezeigt auch zum Thema Observer
Da is ![]() |
AW: Observer-Pattern
OKay, das mit dem .Add etc. sieht da gut aus. Nur ist es in seinem Fall ein konkreter Observer. Ich möchte das als wiederverwendbare Klasse machen. Wenn ich es als generische Klasse mache
Delphi-Quellcode:
kann ich T ja nicht aufrufen, weil nicht klar ist, dass es eine Methode ist. Und ein Constraint, dass da nur Methodentypen reindürfen kenne ich leider nicht.
TObserver<T> = class
Ein Idee wie ich das umschiffe? Dann könnte ich das mit Stevies Lösung verbinden und wäre glücklich :-D |
AW: Observer-Pattern
Zitat:
Dadurch, dass die Eigenschaft Invoke von T ist, kann man die auch aufrufen (z.B.
Delphi-Quellcode:
).
e.Invoke(Button1);
|
AW: Observer-Pattern
Aber ich scheitere doch schon zur DesignTime, weil ich die Notify-Methode nicht programmieren kann.
Delphi-Quellcode:
unit thObserver;
interface uses System.Generics.Collections, Rtti; type TObserver<T> = class private FEvents: TList<T>; public constructor Create; destructor Destroy; override; procedure Notify(Args: array of TValue); property OnEvent: TList<T> read FEvents; end; implementation { TObserver<T> } constructor TObserver<T>.Create; begin FEvents := TList<T>.Create; end; destructor TObserver<T>.Destroy; begin FEvents.Free; inherited; end; procedure TObserver<T>.Notify(Args: array of TValue); var Event: T; begin for Event in FEvents do begin Event(Args); /// Bis hierhin ist noch alles gut, aber das hier geht ja nicht end; end; end. |
AW: Observer-Pattern
Zitat:
![]() |
Alle Zeitangaben in WEZ +1. Es ist jetzt 14:32 Uhr. |
Powered by vBulletin® Copyright ©2000 - 2025, Jelsoft Enterprises Ltd.
LinkBacks Enabled by vBSEO © 2011, Crawlability, Inc.
Delphi-PRAXiS (c) 2002 - 2023 by Daniel R. Wolf, 2024-2025 by Thomas Breitkreuz