Feuilles de root

Logiciels libres, programmation et économie

Accueil » Programmation » Patrons de conceptions Design patterns » Le patron de conception Observateur en Free Pascal

Le patron de conception Observateur en Free Pascal

Reprenons l'exemple proposé dans le livre Game Programming Patterns. Une implémentation en Free Pascal serait la suivante.

On définit une interface IObserver.

{ IObserver }

IObserver = interface
    procedure OnNotify(AEvent : TEvent);
end;

L'exemple du livre utilise une classe abstraite. En Free Pascal on utilisera une interface. Le type TEvent est un type énuméré, l'équivalent d'un enum en C/C++.

TEvent = (eStartFall);

Cette interface définit une procédure OnNotify qui lui permet d'être informée d'un événement.

La méthode de notification est appelée par l'objet observé. Cet objet est appelé Subject par le Gang of Four. Nous définissons donc une classe TSubject. Cette classe a deux fonctions. Premièrement, elle maintient une liste d'observateurs. L'exemple du livre utilise un tableau mais je propose ici d'utiliser une liste :

TObserverList = specialize TGlist<IObserver>;
{ TSubject }

TSubject = class
private
    FObservers : TObserverList;
end;

Le type générique TGlist:[] est une amélioration de la classe TFPGList disponible dans l'unité fgl.

La classe TSubject fournit une interface publique pour ajouter ou retirer des observateurs.

{ TSubject }

TSubject = class
private
    FObservers : TObserverList;
public
    constructor Create;
    procedure AddObserver(AObserver : IObserver);
    procedure RemoveObserver(AObserver : IObserver);
    destructor Destroy; override;
end;

Il est nécessaire de créer la liste dans le constructeur et de la libérer dans le destructeur :

constructor TSubject.Create;
begin
    FObservers := TObserverList.Create;
    FObservers.Duplicates := dupAccept;
end;

destructor TSubject.Destroy;
begin
    FObservers.Clear;
    FObservers.Free;
    inherited Destroy;
end;

Comme nous le verrons plus loin, il est important ici d'appeler la méthode Clear pour vider la liste avant de la libérer.

{ TSubject }

TSubject = class
private
    FObservers : TObserverList;
protected
    procedure Notify(AEvent : TEvent);
public
    constructor Create;
    procedure AddObserver(AObserver : IObserver);
    procedure RemoveObserver(AObserver : IObserver);
    destructor Destroy; override;
end;

Les procedures AddObserver et RemoveObserver sont implémentées de la manière suivante :

procedure TSubject.AddObserver(AObserver : IObserver);
begin
    FObservers.Add(AObserver);
end;

procedure TSubject.RemoveObserver(AObserver : IObserver);
var
    LIndex : Integer;
begin
    if FObservers.Contains(AObserver) then
    begin
        LIndex := FObservers.IndexOf(AObserver);
        FObservers[LIndex] := nil;
        FObservers.Delete(LIndex);
    end;
end;

Comme la liste FObservers contient des interfaces, il faut passer la valeur à nil pour utiliser le compteur de références.

L'autre fonction de la classe TSubject est d'envoyer des notifications :

{ TSubject }

TSubject = class
private
    FObservers : TObserverList;
protected
    procedure Notify(AEntity : TEntity; AEvent : TEvent);
public
    constructor Create;
    procedure AddObserver(AObserver : IObserver);
    procedure RemoveObserver(AObserver : IObserver);
    destructor Destroy; override;
end;

{ TSubject }

procedure TSubject.Notify(AEntity : TEntity; AEvent : TEvent);
var
    i : Integer;
begin
    for i := 0 to FObservers.Count - 1 do
        FObservers[i].OnNotify(AEntity, AEvent);
end;

Imaginons la classe suivante pour représenter une entité du jeu :

TEntity = class
public
    function IsHero : Boolean;
end;

Toute classe qui implémente l'interface IObserver devient un observateur.

{ TAchievements }

TAchievements = class(TInterfacedObject, IObserver)
private
    IsHeroOnBridge : Boolean;
    procedure Unlock(AAchievement : TAchievement);
public
    procedure OnNotify(AEntity : TEntity; AEvent : TEvent);
end;

Reprenons l'exemple d'implémentation de la méthode OnNotify, traduit en Pascal :

{ TAchievements }

procedure TAchievements.OnNotify(AEntity : TEntity; AEvent : TEvent);
begin
    case AEvent of
    eStartFall :
        begin
            if AEntity.IsHero and IsHeroOnBridge then
            begin
                Unlock(aFellOfBridge);
                Break;
            end;
        end;
end;

Les paramètres de Notify sont à adapter. Ici on passe en paramètres une entité du jeu et un message en utilisant un type énuméré.

Les paramètres typiques sont l'objet qui a envoyé la notification et un paramètre générique de données qui contient d'autres informations détaillées.

On peut définir une interface IEvent pour représenter un événement.

{ IEvent }

IEvent = interface
    function GetType : String;
end;

Non seulement cela permet de transmettre des données supplémentaires aux observateurs mais ceux-ci peuvent modifier ces informations, permettant de créer une chaîne de responsabilité.

L'interface IObserver devient :

{ IObserver }

IObserver = interface
    procedure OnNotify(AEvent : IEvent);
end;

La déclaration de la classe TSubject est modifiée comme suit :

{ TSubject }

TSubject = class
private
    FObservers : TObserverList;
protected
    procedure Notify(AEvent : IEvent);
public
    constructor Create;
    procedure AddObserver(AObserver : IObserver);
    procedure RemoveObserver(AObserver : IObserver);
    destructor Destroy; override;
end;

L'implémentation de la méthode Notify devient :

{ TSubject }

procedure TSubject.Notify(AEvent : IEvent);
var
    i : Integer;
begin
    for i := 0 to FObservers.Count - 1 do
        FObservers[i].OnNotify(AEvent);
end;

Les événements sont des classes qui implémentent l'interface IEvent :

TEntityFell = class(TInterfacedObject, IEvent)
private
    FEntity : TEntity;
public
    constructor Create(AEntity : TEntity);
    function GetType : String;
end;

constructor TEntityFell.Create(AEntity : TEntity);
begin
    FEntity := AEntity;
end;

Chaque classe implémentant IEvent représente un type d'événement particulier et contiendra les données à transmettre.

Une dernière amélioration possible consiste à faire de la procédure Notify une fonction qui retourne un booléen, indiquant si la propagation de l'événement doit se poursuivre.

IObserver = interface
    function OnNotify(AEvent : IEvent = nil) : Boolean;
end;
function TSubject.Notify(AEvent : IEvent) : Boolean;
var
    i : Integer;
begin
    Result := True;
    for i := 0 to FObservers.Count - 1 do
    begin
        if not FObservers[i].OnNotify(AEvent) then
        begin
            Result := False;
            Break;
        end;
    end;
    AEvent := nil;
end;

Source:[]

Une autre implémentation en Delphi/Free Pascal