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.