[Delphi] Design Patterns – Observer

[Delphi] Design Patterns - Observer

Olá, leitores!
De todos os Design Patterns abordados até o momento, talvez o Observer seja um dos mais fáceis, tanto de compreender, quanto de implementar. Veremos que a sua proposta é bem interessante em relação à comunicação entre objetos. Durante o artigo, é possível que vocês lembrem ou identifiquem situações em que este padrão de projeto cairia bem.
Sem mais delongas, apresento-lhes o Observer!

 

A princípio, pela tradução, pode-se imaginar que o objetivo do Observer é propor uma forma de observar um objeto, aguardando que algum evento ocorra. Se é isso que você imaginou, está correto!

O padrão de projeto Observer foi elaborado para permitir que objetos recebam dados ou notificações sem o conhecimento de quem é o objeto emissor. Dessa forma, alcançamos um baixo acoplamento na arquitetura (novamente!), já que fortes dependências não são estabelecidas. A qualquer momento, podemos substituir o emissor ou os receptores sem prejudicar a funcionalidade existente. Pode-se dizer, portanto, que a proposta primária do Observer envolve a recepção de notificações quando determinado evento ocorre em um objeto assistido.

Bom, consigo citar alguns exemplos práticos deste padrão. Uma delas, bem clássica, é o funcionamento do Windows Explorer. Faça um teste: abra três janelas no mesmo diretório e crie um arquivo em uma delas. Em seguida, veja que as outras duas janelas automaticamente exibirão o arquivo criado, sem a necessidade de atualizá-las. Isso ocorre porque o diretório é o objeto “observável”, enquanto as janelas são os objetos “observadores”.

A minha esposa encontrou um exemplo ainda melhor. O YouTube fornece uma funcionalidade de inscrição em canais para que os inscritos recebam notificações de novos vídeos publicados, como se estivessem observando o canal. Neste caso, o canal atua como “observável” e os inscritos são os “observadores”.

Um último exemplo, mais voltado para o âmbito técnico, é o padrão de arquitetura MVC (Model-View-Controller). Geralmente, a camada View comporta-se como observadora das mudanças de comportamento que ocorrem no Controller, resultando em camadas bastante desacopladas e sem dependências explícitas.
Garanto que, de agora em diante, você certamente irá encontrar inúmeras aplicações do Observer por aí. 🙂

Você deve ter notado que escrevi “observável” e “observador” entre aspas. Foi proposital. No contexto do Observer, estes elementos recebem nomes diferentes, nos quais utilizaremos até o final do artigo:

  • Subject: Interface que define a assinatura de métodos das classes que serão observáveis;
  • Concrete Subject: implementação da Interface Subject;
  • Observer: Interface que define a assinatura de métodos das classes que serão observadoras;
  • Concrete Observer: implementação da Interface Observer;

 

Para exemplificar o Observer em um ambiente prático, pensei em um controle financeiro. A ideia é simples: quando uma nova operação financeira for cadastrada, a aplicação terá que atualizar os dados em painéis distintos: o balanço financeiro, os valores de débitos agrupados por categoria e um log do histórico de operações. Cada um destes painéis serão VCL Frames. Optei por este componente justamente para demonstrar a comunicação que ocorre entre objetos que não se referenciam.
Acredito que, só com essas informações, já ficou fácil identificar os elementos, não é? O cadastro de operações atuará como Concrete Subject, enquanto os painéis serão Concrete Observers.

A primeira etapa é modelar uma estrutura que não faz parte do contexto do Observer, mas julgo importante implementá-la. Trata-se de um record que contém atributos para armazenar os dados que serão enviados na notificação, comportando-se como um objeto de “transporte” de dados:

type
  TNotificacao = record
    Operacao: string;
    Categoria: string;
    Valor: real;
  end;

 

A segunda etapa é criar a Interface Observer. Veja, a seguir, que há apenas um método, no qual será chamado automaticamente quando houver uma nova notificação:

type
  IObserver = interface
    procedure Atualizar(Notificacao: TNotificacao);
  end;

 

A terceira etapa, talvez, é a mais morosa. Criaremos os Concrete Observers, que serão os VCL Frames, lembrando que cada um deles deverá implementar a Interface acima, implicando, claro, na declaração do método Atualizar. Para que não fique tão extenso, decidi inserir a imagem do frame acompanhada do código-fonte. Além disso, como o exemplo é didático, não me preocupei com strings e números mágicos, ok?

Frame Observer - Balanço Financeiro

type
  TFrameSaldo = class(TFrame, IObserver)
  ...
  private
    FCreditos: real;
    FDebitos: real;
  public
    procedure Atualizar(Notificacao: TNotificacao);
  end;
 
{ ... }
 
procedure TFrameSaldo.Atualizar(Notificacao: TNotificacao);
var
  Saldo: real;
begin
  // Soma o valor à variável conforme o tipo de operação
  if Notificacao.Operacao = 'Crédito' then
    FCreditos := FCreditos + Notificacao.Valor
  else if Notificacao.Operacao = 'Débito' then
    FDebitos := FDebitos + Notificacao.Valor;
 
  // Calcula o saldo
  Saldo := FCreditos - FDebitos;
 
  LabelValorCreditos.Caption := FormatFloat('###,##0.00', FCreditos);
  LabelValorDebitos.Caption := FormatFloat('###,##0.00', FDebitos);
  LabelValorSaldo.Caption := FormatFloat('###,##0.00', Saldo);
end;

 

Frame Observer - Agrupamento de Débitos

type
  TFrameAgrupamento = class(TFrame, IObserver)
  ...
  public
    procedure Atualizar(Notificacao: TNotificacao);
  end;
 
{ ... }
 
procedure TFrameAgrupamento.Atualizar(Notificacao: TNotificacao);
begin
  if Notificacao.Operacao = 'Crédito' then
    Exit;
 
  // Encontra a categoria de débito para somar o valor
  if ClientDataSet.Locate('Categoria', Notificacao.Categoria, []) then
  begin
    ClientDataSet.Edit;
 
    ClientDataSet.FieldByName('Total').AsFloat :=
      ClientDataSet.FieldByName('Total').AsFloat + Notificacao.Valor;
    ClientDataSet.Post;
 
    Exit;
  end;
 
  // Cadastra a categoria caso ela ainda não exista no agrupamento
  ClientDataSet.AppendRecord([Notificacao.Categoria, Notificacao.Valor]);
end;

 

Frame Observer - Log de Operações

type
  TFrameLog = class(TFrame, IObserver)
  ...
  public
    procedure Atualizar(Notificacao: TNotificacao);
  end;
 
{ ... }
 
procedure TFrameLog.Atualizar(Notificacao: TNotificacao);
var
  TextoLog: string;
begin
  TextoLog := Format('Uma operação de %s de %.2f foi adicionada',
    [Notificacao.Operacao, Notificacao.Valor]);
 
  MemoLog.Lines.Add(TextoLog);
end;

 

A quarta etapa é codificar a Interface Subject, que deve obrigatoriamente providenciar três métodos essenciais para adicionar, remover e notificar Observers:

type
  ISubject = interface
    procedure AdicionarObserver(Observer: IObserver);
    procedure RemoverObserver(Observer: IObserver);
    procedure Notificar;
  end;

 

Para que o exemplo fique ainda mais desacoplado, o Concrete Subject também será um VCL Frame, porém, a codificação é um pouco mais extensa. No Concrete Object, devemos utilizar uma lista de objetos responsável por armazenar os Observers registrados. Os métodos AdicionarObserver e RemoverObserver, portanto, terão a função de manipular essa lista, incluindo ou excluindo os Observers.

Frame Subject - Cadastro de Operações Financeiras

type
  TFrameCadastroOperacoes = class(TFrame, ISubject)
  private
    FObservers: TList<IObserver>;
  public
     constructor Create(AOwner: TComponent) ; override;
     destructor Destroy; override;
 
    procedure AdicionarObserver(Observer: IObserver);
    procedure RemoverObserver(Observer: IObserver);
  end;  
 
{ ... }
 
constructor TFrameCadastroOperacoes.Create(AOwner: TComponent);
begin
  inherited;
  FObservers := TList<IObserver>.Create;
end;
 
destructor TFrameCadastroOperacoes.Destroy;
begin
  FObservers.Free;
  inherited;
end;
 
procedure TFrameCadastroOperacoes.AdicionarObserver(Observer: IObserver);
begin
  FObservers.Add(Observer);
end;
 
procedure TFrameCadastroOperacoes.RemoverObserver(Observer: IObserver);
begin
  FObservers.Delete(FObservers.IndexOf(Observer));
end;

 

Não terminamos por aí. Codificaremos, por último, o método Notificar, que merece uma atenção. Nele, um objeto do tipo TNotificacao será declarado, preenchido, e enviado para cada Observer registrado na lista:

private
    procedure Notificar;
 
{ ... }
 
procedure TFrameCadastroOperacoes.Notificar;
var
  Notificacao: TNotificacao;
  Observer: IObserver;
begin
  Notificacao.Operacao := ClientDataSet.FieldByName('Operacao').AsString;
  Notificacao.Categoria := ClientDataSet.FieldByName('Categoria').AsString;
  Notificacao.Valor := ClientDataSet.FieldByName('Valor').AsFloat;
 
  for Observer in FObservers do
  begin
    Observer.Atualizar(Notificacao);
  end;
end;

Este método será chamado quando o botão “Gravar” for acionado (vide imagem acima):

begin
  // Adiciona um registro no DataSet conforme valores informados pelo usuário
  ClientDataSet.AppendRecord(
    [ComboBoxOperacao.Text, ComboBoxCategoria.Text, StrToFloatDef(EditValor.Text, 0)]);
 
  // Chama o método de notificação
  Notificar;
end;

 

Tudo agora faz sentido. Ao clicar em “Gravar”, o Concrete Subject chamará o método Atualizar de cada Concrete Observer, enviando os dados da operação financeira. Cada um deles receberá a notificação, comportando-se conforme o que estão destinados a processar. Bem interessante, não?

O último passo é juntar tudo!
Em um formulário (Client), adicionei os quatro frames que criamos neste artigo: um Concrete Subject e três Concrete Observers. No evento OnCreate do formulário, montaremos as peças dessa arquitetura, adicionando os três frames Observers na lista do Concrete Subject:

FrameCadastroOperacoes.AdicionarObserver(FrameSaldo);
FrameCadastroOperacoes.AdicionarObserver(FrameAgrupamento);
FrameCadastroOperacoes.AdicionarObserver(FrameLog);

 

Ao executar o projeto e adicionar algumas operações, observe que todos os frames se comunicam, trabalhando em conjunto, como se estivessem em uma mesma unit:

Exemplo de aplicação utilizando o padrão de projeto Observer

Faço questão de repetir: nenhum deles estão explicitamente referenciados. Na unit do Concrete Subject, não adicionamos a referência dos três frames observadores na seção uses. Do mesmo modo, nas units dos Concrete Observers, não adicionamos a referência do frame observável. Eles não se conhecem, mas se comunicam perfeitamente, resultando em um nível baixíssimo de acoplamento.

 

Você esqueceu de usar o método “RemoverObserver”?
Não, não esqueci. Poderíamos ter codificado um mecanismo pra remover os Observers em tempo de execução, porém, demandaria algumas linhas extras de codificação. Este método é conveniente para cenários em que um dos Observers pode decidir não receber mais as notificações. Uma boa analogia são as notificações do Facebook quando algum evento ocorre na linha do tempo (comentários, curtidas ou compartilhamentos). A qualquer momento, o usuário pode desligar as notificações de uma publicação específica, simulando um comportamento do método RemoverObserver.

Enfim, leitores, isso é o que deixo do Observer para vocês. Este padrão de projeto é largamente utilizado em função da sua proposta de desacoplamento e facilidade na propagação de dados. Além disso, claro, vale reforçar a ideia de que os elementos concretos da arquitetura não se conhecem, tornando-a bastante abstrata.

O projeto de exemplo, com algumas melhorias técnicas, está disponível no GitHub:

https://github.com/AndreLuisCelestino/Delphi-DesignPatterns/tree/master/Observer-AndreCelestino

 

Grande abraço, pessoal!


 

Compartilhe!
Share on FacebookTweet about this on TwitterShare on LinkedInShare on Google+Pin on PinterestEmail this to someone

8 comentários

  1. Boa noite André, muito esclarecedor seus artigos, exemplos fáceis de entender.
    Parabéns e muito obrigado

  2. Bom dia!
    Eu particularmente acho o padrão Observer uma verdadeira “obra de arte”, para ficar do lado na Monalisa! kkkkkk
    Esse padrão a meu ver é fantástico!
    Obrigado por essa série!
    Grande abraço, meu amigo!

    1. Olá, Elton!
      Eu que agradeço por acompanhar a série de artigos!
      O Observer é um dos meus padrões de projetos favoritos também. Gosto bastante da sua proposta de solução.
      Abraço!

  3. Salve, André!

    Como é bonito ver um código bem escrito como nos exemplos que vc apresenta. Continue com esse ótimo trabalho que vc faz, eu realmente tenho aprendido muito com vc e sua série sobre design patterns.

    Abraço!

  4. Salve André. Cara, esse pattern veio bem na hora. Já tinha até me esquecido como implementava observer no Delphi, visto que temos Rx no Java/Android e Swift nós esquecemos como tudo funciona por trás. Parabéns por esta série de artigos. Acompanho sempre. Sucesso meu amigo. Abraço.

    1. Fala, Marcos!
      Na minha opinião, este é um dos Design Patterns mais fáceis de implementar, não só no Delphi, mas em qualquer linguagem orientada a objetos.
      Aproveitando, eu lembro de uma vez que você comentou sobre o Observer comigo, destacando as vantagens dele e de outros padrões de projeto. 🙂
      Obrigado por acompanhar a série! Abraço!

Deixe uma resposta

O seu endereço de e-mail não será publicado. Campos obrigatórios são marcados com *

Preencha o campo abaixo * Time limit is exhausted. Please reload CAPTCHA.