29 de abril de 2011

Design Patterns com Delphi: Observer - Parte II

No post anterior, apresentei o tipo de problema que o Design Pattern comportamental Observer se propõe a resolver, introduzindo os conceitos envolvidos na implementação de uma solução. Aqui, mostro uma sugestão de como efetivamente implementá-lo em projetos Delphi.

Dependendo do contexto onde o padrão Observer será empregado, a implementação dele poderá sofrer ligeiras variações. Uma das diferença mais comuns está relacionada a quantas instâncias de classes poderão ser notificadas sobre um mesmo evento. Eventos da VCL, por exemplo, podem ser respondidos por no máximo uma única função, vinculada a uma classe. Por isso, uma variável simples é o bastante para conter a referência da função que será chamada. Já no exemplo que aparece no post anterior, diversos usuários podem requerer notificação para a mesma atualização num objeto de negócio. Por isso, nossa implementação precisará armazenar os observers em algum tipo de lista.

Para facilitar, reproduzo abaixo o diagrama UML representando a proposta de implementação do exemplo do post anterior:
Diagrama UML para o padrão Observer

Pelo diagrama, a classe que define o comportamento base de todo Subject é a TWBusinessObj. É ela, portanto, quem tem que ser capaz de armazenar as instâncias dos Observer que receberão avisos de alteração. Veja no código abaixo que o controle de quais instâncias devem receber os avisos é feito com uma lista de objetos, ou seja, tantas instâncias quantas forem necessárias podem "assinar" o serviço e serem avisadas :
TWBOState=class;
TWBOObserver=class;

TWBusinessObj=class
protected
_ListaObsv : TObjectList;
{ ... }
public
procedure AttachObsv (AObsv: TWBOObserver);
procedure DetachObsv (AObsv: TWBOObserver);
procedure Notify;

function getState: TWBOState;virtual;abstract;

{ ... }

Constructor Create;
Destructor Destroy;override;
end;

implementation

Constructor TWBusinessObj.Create;
begin
_ListaObsv := TObjectList.Create (false);
end;

Destructor TWBusinessObj.Destroy;
begin
FreeAndNil (_ListaObsv);
inherited;
end;

procedure TWBusinessObj.AttachObsv (AObsv: TWBOObserver);
begin
_ListaObsv.Add(AObsv);
end;

procedure TWBusinessObj.DetachObsv (AObsv: TWBOObserver);
begin
_ListaObsv.Remove(AObsv);
end;

procedure TWBusinessObj.Notify;
var i : integer;
lObsv : TWBOObserver;
begin
for i := 0 to _ListaObsv.Count - 1 do
try
lObsv := _ListaObsv.Items [i] As TWBOObserver;
lObsv.Update(Self);
except
end;
end;

Há outros detalhes para prestar atenção. O construtor da lista recebe um parâmetro com valor false para indicar que ela não é responsável por devolver a memória utilizada pelos objetos que ela contém. Isso não pode acontecer pois ainda podemos precisar dos objetos inseridos na lista mesmo que o TWBusinessObj seja deletado. Lembre-se de que, neste exemplo, os objetos que receberão as notificações são instâncias que representam usuários no sistema e que eles podem requerer notificações de vários objetos de negócio diferentes ao mesmo tempo.

Um outro ponto é que a classe TWBusinessObj é abstrata e, portanto, não poderá ser diretamente instanciada. Cada herança dela precisará remover a abstração, implementando a função getState para fornecer informações específicas sobre o estado interno de suas propriedades. Veja um exemplo:
TWPedidoVenda=class(TWBusinessObj)
protected
{ ... }
_State : TWBOState;
public
{ ... }
function getState : TWBOState;override;
procedure setDataEntrega (ADt: TDateTime);
end;

{ ... }
function TWPedidoVenda.getState : TWBOState;
begin
Result := _State;
end;

procedure TWPedidoVenda.setDataEntrega (ADt: TDateTime);
begin
_State.ModifyProperty ('DT_ENTREGA', ADt);
Notify;
end;

A classe TWBOState que aparece nesse código é uma base que eu montei para conter as informações de estado específicas de cada objeto de negócio, permitindo recuperar o valor atual de cada propriedade e aquele que havia antes, caso ela tenha sofrido uma alteração. No trecho mostrado acima, uma instância dessa classe registra a alteração na data de entrega do pedido e, em seguida, o próprio pedido dispara notificações dessa alteração usando a função Notify mostrada no primeiro quadro desse post.

Fica faltando, então, a segunda parte da operação: fazer com que cada Observer interprete como lhe for conveniente as modificações notificadas, implementando a função Update:
TWBOObserver=class
public
procedure Update (AObj: TWBusinessObj);virtual;abstract;
end;

TWAuditoria=class(TWBOObserver)
public
procedure Update (AObj: TWBusinessObj);override;
end;

TWUsuario=class(TWBOObserver)
public
procedure Update (AObj: TWBusinessObj);override;
end;

{ ... }

procedure TWAuditoria.Update (AObj: TWBusinessObj);
var lState : TWBOState;
begin
lState := AObj.getState();
GravaAuditoria (lState);
end;

procedure TWUsuario.Update (AObj: TWBusinessObj);
var lState : TWBOState;
begin
lState := AObj.getState();
EnviaEMail (lState);
end;

Em algum ponto do sistema teremos que usar a função AttachObsv para vincular a classe de auditoria e os usuários apropriados, isto é, fazer com que essas classes "assinem" o serviço de notificação de um objeto de negócio:
var _Auditoria : TWAuditoria;
_Gerente : TWUsuario;
{ ... }
Constructor TWFormVenda.Create (AOwner: TComponent);
begin
inherited;
_Pedido := TWPedidoVenda.Create;
_Pedido.AttachObsv (_Auditoria);
_Pedido.AttachObsv (_Gerente);
{ ... }
end;

O planejamento do exemplo incluiu ainda a possibilidade de uma classe cancelar a assinatura, suspendendo as notificações para si. Para isso, basta chamar a função DetachObsv quando (e se) for necessário.

2 comentários :

Unknown disse...

Olá Luis,

Como sempre, seus posts me salvando.
Poderia fornecer o código da classe TWBOState? Ou os fontes desse exemplo? Não consegui implementar esse exemplo, fiquei com dúvidas em relação a classe TWBOState.

Obrigado!

Luís Gustavo Fabbro disse...

Dyego

Eu não tenho mais o código usado nos exemplos; eles estavam guardados nos servidores do Google e foram perdidos quando eles removeram a conta do blog (por causa da idade, o Google decidiu que o blog não pode usar os serviços deles).

Em todo caso, o TWBOState pode ser implementado como uma estrutura simples, contendo o nome da propriedade modificada e o novo valor que ela está assumindo, usando talvez um Variant para armazenar dados de qualquer tipo. Se necessário, pode ser ampliada para reportar também o valor antigo da propriedade; isso faz mais sentido qdo se trata de auditoria.

[]s

Postar um comentário

OBS: Os comentários enviados a este Blog são submetidos a moderação. Por isso, eles serão publicados somente após aprovação.

Observação: somente um membro deste blog pode postar um comentário.