sexta-feira, 4 de outubro de 2013

Padrão de projeto

Observer


O padrão observer é utilizado quando se há a necessidade de que uma classe notifique um grupo de outras classes de mudanças em seu estado. Vou dar um exemplo simples que será um relógio digital. Vamos supor que você tenha uma classe que represente um relógio e você queira criar um widget personalizável que exiba este relógio de várias formas, analógico, digital, binário(para nerds), ao mesmo tempo.... Na maneira tradicional de fazer isso, você implementaria o código necessário para pegar a hora do sistema e fazer a exibição destes dados em cada um dos tipos de relógio causando repetição de código. Com o padrão observer a classe do relógio, o sujeito(subject) observado, notifica às classes de exibição que, por sua vez, são suas observadoras, de que a hora mudou e que elas devem atualizar. Desta forma o relógio é um só, mas as maneiras de exibir podem ser várias. No fundo, o que se criou, é uma relação um para muitos do objeto sujeito em relação aos observadores.

Este é apenas um exemplo simples do que este padrão é capaz de fazer. Mas vamos aos códigos necessários.

O fonte abaixo representa a interface IClock que diz que todo relógio deve 'ticar'

unit ifClock;

interface

uses ifClockObserver;

type
  IClock = Interface(IClockSubject)
    ['{36F4E7EE-9B58-4160-BD08-1AB8BF484AA1}']
    procedure Tick;
  End;

implementation

end.

O fonte ifClockObserver descreve o que é necessário para que o padrão observer funcione.

unit ifClockObserver;

interface

type 
 
  IClockObserver = Interface
    ['{43495835-B5D9-4A1E-A0FD-87C2D7AFEE86}']
    procedure TimeChanged(ATime: TDateTime);
  End;

  IClockSubject = Interface
    ['{CEBF5998-3B28-42E7-94A3-D9DCA400F155}']
    procedure RegisterObserver(AClockObserver: IClockObserver);
    procedure RemoveObserver(AClockObserver: IClockObserver);
    procedure NotifyTimeChanged;
  End;
implementation

end.

Repare que a descrição das interfaces coloca um IClockSubject e um IClockObserver.

Note bem o método TimeChanged(ATime: TDateTime) da interface IClockObserver, é através dele que a classe de exibição que implementar esse método receberá a notificação de alteração da hora do relógio e fazer sua própria atualização.

Agora vamos aos métodos da interface IClockSubject:

O procedimento RegisterObserver(AClockObserver: IClockObserver) é o responsável por "injetar" o observador dentro do sujeito a ser observado, de forma que se possa chamar o método TimeChanged(ATime: TDateTime) de dentro da classe do sujeito observado, neste caso, o relógio.

Já o procedimento RemoveObserver(AClockObserver: IClockObserver) é o responsável por dizer ao observador de que a classe observadora não "tem mais interesse" em receber notificações de suas mudanças de estado.

E, finalmente, o procedimento NotifyTimeChanged é o responsável por percorrer a lista de observadores da classe observada e executar o método TimeChanged(ATime: TDateTime) em cada um deles.


A unit Global.pas é a responsável por guardar a instância do relgógio de forma global, para que todos os exibidores o enxerguem. O formulário principal da aplicação será o responsável por instanciar o objeto TClock.
unit Global;

interface

uses ifClock;

var
  Clock: IClock;

implementation

end.
Vamos ao fonte da classe do relógio, como ele é um relógio e um relógio "tica", logo ele implementa a interface IClock, como ele será um sujeito a ser observado, então implementa, também a interface IClockSubject. Quanto à classe TInterfaceObject é uma classe padrão do Delphi que cuida de objetos que são instanciados através de suas interfaces de forma a fazer a liberação de memória do objeto, automaticamente, quando o número de referências à ele chega a zero, ou seja, não se usa o método free. Ao invés disso, toda vez que não for mais utilizar a instância do objeto, atribua nil para sua variável.

unit clClock;

interface

uses
  ifClock, ifClockObserver, Classes, Vcl.ExtCtrls;

type
  TClock = class(TInterfacedObject, IClock, IClockSubject)
  private
//Lista da interface de observadores do relógio
    FObservers: IInterfaceList; //Interface da unit System.Classes
//Utilizando o timer do VCL, componente de formulário, para fazer o relógio
//"ticar" a cada um segundo
    FTimer: TTimer;
//Implementação do método OnTimer do TTimer
    procedure OnTimer(Sender: TObject); 
//Responsável por fazer o relógio "ticar"
    procedure Tick;
//Implementação do método RegisterObserver, responsável por registrar as
    procedure RegisterObserver(AClockObserver: IClockObserver); 
//classes de exibição para serem notificadas da atualização da hora
//Implementação do método RemoveObserver responsável por desregistrar o
//observador que não mais "se interessar" por receber notificações
    procedure RemoveObserver(AClockObserver: IClockObserver); 
//Implementação do método NotifyTimeChanged responsável por notificar à
//lista de observadores a mudança da hora
    procedure NotifyTimeChanged;
  public
    constructor Create;
    destructor Destroy; override;
  end;

implementation

uses
  SysUtils;

constructor TClock.Create;
begin
//executa o construtor padrão da classe mãe
//SEMPRE CHAME PRIMEIRO O CONSTRUTOR DA CLASSE MÃE
  inherited; 
//Instancia o timer
  FTimer := TTimer.Create(nil);
//Configura o timer para um intervalo de 1s
  FTimer.Interval := 1000;
//Atribui o evento OnTimer do timer para a implementação feita pela
//classe
  FTimer.OnTimer := OnTimer;
//Habilita o timer
  FTimer.Enabled := True;
end;

destructor TClock.Destroy;
begin
//Libera o timer da memória para não haver memory leaks, princípio de OO,
//criou um objeto, libere sua instância quando não for mais utilizar
  FreeAndNil(FTimer);
//Executa o destrutor padrão da classe mãe, SEMPRE CHAME O DESTRUTOR DA
//CLASSE MÃE POR ÚLTIMO
  inherited;
end;

procedure TClock.OnTimer(Sender: TObject);
begin
  Tick; //Faz com que o método do timer execute o método Tick do relógio
end;

procedure TClock.Tick;
begin
//O método tick executa o método NotifyTimeChanged a fim de avisar aos
//observadores que a hora foi atualizada
  NotifyTimeChanged;
end;

procedure TClock.RegisterObserver(AClockObserver: IClockObserver);
begin
//Se a lista de observadores ainda não foi instanciada, instancie
  if not Assigned(FObservers) then
    FObservers := TInterfaceList.Create;

//Se o observador ainda não foi adicionado na lista, adicione
  if FObservers.IndexOf(AClockObserver) = -1 then
    FObservers.Add(AClockObserver);
end;

procedure TClock.RemoveObserver(AClockObserver: IClockObserver);
begin
//Se a lista de observadores estiver instanciada, remova o observador da lista
  if Assigned(FObservers) then
    FObservers.Remove(AClockObserver);

  if FObservers.Count = 0 then
//Não se usa free em interfaces, o Delphi verifica o número de referências
//à uma interface, quando chega à zero, o Delphi libera a memória do objeto
//automaticamente. Sendo assim, basta atribuir nil para liberar o objeto da
//memória
    FObservers := nil; 
end;

procedure TClock.NotifyTimeChanged;
var
  IntI_F: Integer; //Contador que percorrerá a lista de observadores 
begin
//Se a lista de observadores foi instanciada
  if Assigned(FObservers) then
  begin
//percorra a lista de observadores
    for IntI_F := 0 to Pred(FObservers.Count) do 
    begin
//Notifique os observadores de que a hora mudou executando o método
//TimeChanged de cada um deles para que possam se atualizar.
//Repare que foi necessário uma conversão de tipos, pois cada um dos itens
//contidos nesta lista são armazenados como IInterface e não o tipo
//IClockObserver
      (FObservers.Items[IntI_F] as IClockObserver).TimeChanged(Now);
    end;
  end;
end;

end.
Agora vamos ao fonte das classes de exibição, neste caso utilizei formulários mesmo com labels. São dois formulários o FRMObserver1 e o FRMObserver2. O primeiro é o principal da aplicação e instanciará o relógio, além de observá-lo para atualizar seu label com a hora corrente. O segundo irá apenas observar o relógio já instanciado e atualizará seu label.
unit FRMObserver1;

interface

uses
  Winapi.Windows, Winapi.Messages, System.SysUtils, System.Variants,
  System.Classes, Vcl.Graphics, Vcl.Controls, Vcl.Forms, Vcl.Dialogs,
  Vcl.StdCtrls,

  ifClockObserver, ifClock, clClock, FRMObserver2;

type
//Repare na declaração da classe que foi acrescentado um IClockObserver
//É necessário para tornar o formulário um observador do relógio
//através da implementação do método TimeChanged
  TForm1 = class(TForm, IClockObserver)
//Label que receberá a hora do relógio
    Label1: TLabel;
    procedure FormCreate(Sender: TObject);
    procedure FormDestroy(Sender: TObject);
    procedure FormShow(Sender: TObject);
  private
    { Private declarations }
//Método que será chamado pelo relógio para notificar este formulário
//quanto à atualização da hora
    procedure TimeChanged(ATime: TDateTime);
  public
    { Public declarations }
  end;

var
  Form1: TForm1;

implementation

{$R *.dfm}

uses Global;

procedure TForm1.FormCreate(Sender: TObject);
begin
//Instancia o relógio que está no global.pas
  Clock := TClock.Create;
//O formulário se registra como observador do relógio
  Clock.RegisterObserver(Self);
end;

procedure TForm1.FormDestroy(Sender: TObject);
begin
//Desregistra o formulário como observador do relógio
  Clock.RemoveObserver(Self);
//Conforme explicado, um objeto TInterfacedObject não deve ser liberado com
//free. Ao invés disso, apenas atribui-se nil para a variável para que a
//contagem de referências caia, se chegar à 0, o delphi mesmo libera
//automaticamente. Como exercício, tente executar clock.free no lugar de
//Clock := nil, haverá exceção de acesso à memória.
  Clock := nil;
end;

procedure TForm1.FormShow(Sender: TObject);
begin
//Exibe o segundo painel de hora para mostrar que o observer está
//notificando ambos formulários
  Form2.Show;
end;

procedure TForm1.TimeChanged(ATime: TDateTime);
begin
//Atualiza o label com a hora atual
  Label1.Caption := DateTimeToStr(ATime);
end;

end.


Agora o fonte do segundo formulário que também é observador, mas utiliza do relógio já instanciado pelo principal.

unit FRMObserver2;

interface

uses
  Winapi.Windows, Winapi.Messages, System.SysUtils, System.Variants, System.Classes, Vcl.Graphics,
  Vcl.Controls, Vcl.Forms, Vcl.Dialogs,Vcl.StdCtrls,

  ifClockObserver, Global;

type
//Repare na declaração da classe que foi acrescentado um IClockObserver
//É necessário para tornar o formulário um observador do relógio
//através da implementação do método TimeChanged
  TForm2 = class(TForm, IClockObserver)
//Label que receberá a hora do relógio
    Label1: TLabel;
    procedure FormCreate(Sender: TObject);
    procedure FormDestroy(Sender: TObject);
  private
    { Private declarations }
    procedure TimeChanged(ATime: TDateTime);
  public
    { Public declarations }
  end;

var
  Form2: TForm2;

implementation

{$R *.dfm}

procedure TForm2.FormCreate(Sender: TObject);
begin
//Registra o formulário como observador do relógio
  Clock.RegisterObserver(Self);
end;

procedure TForm2.FormDestroy(Sender: TObject);
begin
//Desregistra o formulário como observador do relógio
  Clock.RemoveObserver(Self);
end;

procedure TForm2.TimeChanged(ATime: TDateTime);
begin
//Recebe a notificação da atuliazção da hora e atualiza o label
  Label1.Caption := DateTimeToStr(ATime);
end;

end.