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.

Nincsenek megjegyzések:

Megjegyzés küldése