2004. augusztus 28., szombat
The Observer pattern
Problem/Question/Abstract:
How can many objects be notified by an event?
Answer:
Sometimes, there is a need to notify many different objects about a state change. My favorite solution to this problem is the Observer pattern, described in Design Patterns. (I highly recommend this book). Roughly, this patterns describe a way for objects that want to be notified of a change (called the Observers) to register themselves with the subject (called the Subject :). This subject then has the responsability to notify all it's observers when it's internal state changes.
And this is where we encounter our first problem. How does the Subject notify it's Observers? By calling one of the Observer's method. But this means the Subject has to know the Observer somehow, not very flexible. We could use an abstract Observer class, in that way our Subject would always be able to call a known method, but this would force us to always descend observers from the same hiearchy, which is not very practical (and sometimes completely impossible).
Fortunately, for each problem there is a solution, and in our case there are at least TWO!
The first solution is to use Interfaces. Our Subject would accept an IObserver interface. The neat thing about interfaces is that any class can implement any interface, and as long as an object would implement IObserver, it would be able to connect to any subject. (If anyone is interested in this implementation, let me know and I'll post another article, but there are gazillions of examples on the net if you search a little). This works perfectly, but after working with interfaces a little, I decided not to use them. The main reason for this is that code navigation in the IDE is harder, Ctrl-Click brings you to the interface definition, not the implementation. No big deal. But I wanted something better.
The second solution, the one I'm describing in this article, is a little different. The Observer is no longer an object, it's a method. Just like standard VCL events. This means that a single event handler could be used with a component and a Subject at the same time.
Let say we have a TSubject with an event called OnChange. This event is of type TMultiNotifyEvent. Here is how a user would connect to this event. ChangeHandler is a procedure matching a TNotifyEvent.
MySubject.OnChange.Attach(ChangeHandler);
From that point on, every time MySubject changes, it will call ChangeHandler, just like a normal OnChange event. The difference is that there might be many observers of that event. When the object no longer wish to receive updates from MySubject, it detaches:
MySubject.OnChange.Detach(ChangeHandler);
In order for TMySubject to use a TMultiNotifyEvent, it must create it like this:
type
TMySubject = class
private
FOnChange: TMultiNotifyEvent;
protected
procedure DoChange; virtual;
public
constructor Create;
destructor Destroy; override;
property OnChange: TMultiNotifyEvent read FOnChange;
end;
implementation
constructor TMySubject.Create;
begin
inherited;
FOnChange := TMultiNotifyEvent.Create;
end;
destructor TMySubject.Destroy;
begin
FOnChange.Free;
inherited;
end;
procedure TMySubject.DoChange;
begin
{ Signal is the method that notify every observers }
OnChange.Signal(Self);
end;
In order to use a new type of event, one must declare a new class inheriting from TMultiEvent. Why am I doing this? Because TMultiEvent only stores and knows about TMethod records, it does not know what type of event is used, what are it's parameters, etc. By having the SignalObserver method Abstract, each concrete class can typecast it to the type of events it handles and pass all the required parameters. Also, creating a new class provices a certain level of type by making sure you register a compatible method with the subject. See TMultiNotifyEvent at the end of this article to see how to create customized events, it's pretty straight forward.
That's it for the explication, if you have any questions just leave a comment and I'll try to update the article accordingly.
unit uMultiEvent;
interface
uses
Classes, SysUtils;
type
TMultiEvent = class
private
FObservers: TList;
protected
function FindObserver(Observer: TMethod): integer;
function GetObserver(Index: integer): TMethod;
procedure SignalObserver(Observer: TMethod); virtual;
public
constructor Create;
destructor Destroy; override;
procedure Attach(Observer: TMethod);
procedure Detach(Observer: TMethod);
procedure Signal;
end;
TMultiNotifyEvent = class(TMultiEvent)
private
FSender: TObject;
protected
procedure SignalObserver(Observer: TMethod); override;
public
procedure Attach(Observer: TNotifyEvent);
procedure Detach(Observer: TNotifyEvent);
procedure Signal(Sender: TObject);
end;
implementation
{ TEvent }
procedure TMultiEvent.Attach(Observer: TMethod);
var
Index: integer;
begin
Index := FindObserver(Observer);
{ This assertion is facultative, we could just ignore observers }
{ already attached, but it's a good way to detect problems early }
{ and avoid unnecessary processing. }
Assert(Index < 0, 'This observer was already attached to this event');
{ A method contains two pointers: }
{ - The code pointer, that's where the procedure is in memory }
{ - The data pointer, this tells Delphi what instance of the }
{ object calls the procedure }
{ We must store both pointers in order to use that callback. }
if Index < 0 then
begin
FObservers.Add(Observer.Code);
FObservers.Add(Observer.Data);
end;
end;
constructor TMultiEvent.Create;
begin
inherited;
FObservers := TList.Create;
end;
destructor TMultiEvent.Destroy;
begin
{ This assertion is facultative, but I prefer when all my objects }
{ are "clean" when they are destroyed. }
Assert(FObservers.Count = 0, 'Not all observers were detached');
FreeAndNil(FObservers);
inherited;
end;
procedure TMultiEvent.Detach(Observer: TMethod);
var
Index: integer;
begin
Index := FindObserver(Observer) * 2;
{ Again, the assertion is facultative, nothing would be broken }
{ if we just ignored it. }
Assert(Index >= 0, 'The observer was not attached to this event');
if Index >= 0 then
begin
FObservers.Delete(Index); // Delete code pointer
FObservers.Delete(Index); // Delete data pointer
end;
end;
function TMultiEvent.FindObserver(Observer: TMethod): integer;
var
i: integer;
begin
{ Search fails by default, if there is a match, result will be updated. }
Result := -1;
for i := (FObservers.Count div 2) - 1 downto 0 do
begin
{ We have a match only if both the Code and Data pointers are the same. }
if (Observer.Code = FObservers[i * 2]) and (Observer.Data = FObservers[i * 2 + 1])
then
begin
Result := i;
break;
end;
end;
end;
function TMultiEvent.GetObserver(Index: integer): TMethod;
begin
{ Fill the TMethod record with the code and data pointers. }
Result.Code := FObservers[Index * 2];
Result.Data := FObservers[Index * 2 + 1];
end;
procedure TMultiEvent.SignalObserver(Observer: TMethod);
begin
{ Descendants must take care to notify the Observer by themselves }
{ because we cannot know the parameters required by the event. }
Assert(Assigned(@Observer));
{ We could make this method Abstract and force descendants, but }
{ I prefer to do a run-time check to validate the passe methods }
end;
procedure TMultiEvent.Signal;
var
i: integer;
begin
{ Call SignalObserver for each stored observers in reverse order. }
{ SignalObserver (which is declared in sub-classes) will typecast }
{ the TMethod record into whatever procedure type it handles. }
{ See the TMultiNotifyEvent below for an example. }
for i := (FObservers.Count div 2) - 1 downto 0 do
begin
SignalObserver(GetObserver(i));
end;
end;
{ TMultiNotifyEvent }
procedure TMultiNotifyEvent.Attach(Observer: TNotifyEvent);
begin
inherited Attach(TMethod(Observer));
end;
procedure TMultiNotifyEvent.Detach(Observer: TNotifyEvent);
begin
inherited Detach(TMethod(Observer));
end;
procedure TMultiNotifyEvent.Signal(Sender: TObject);
begin
FSender := Sender;
inherited Signal;
end;
procedure TMultiNotifyEvent.SignalObserver(Observer: TMethod);
begin
inherited;
TNotifyEvent(Observer)(FSender);
end;
end.
Feliratkozás:
Megjegyzések küldése (Atom)
Nincsenek megjegyzések:
Megjegyzés küldése