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!
Introdução
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í. 🙂
Estrutura
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;
Exemplo de codificação do 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:
1 2 3 4 5 6 |
type TNotificacao = record Operacao: string; Categoria: string; Valor: real; end; |
Interface Observer
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:
1 2 3 4 |
type IObserver = interface procedure Atualizar(Notificacao: TNotificacao); end; |
Classes Concrete Observers
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?
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
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; |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
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; |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
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; |
Interface Subject
A quarta etapa é codificar a Interface Subject, que deve obrigatoriamente providenciar três métodos essenciais para adicionar, remover e notificar Observers:
1 2 3 4 5 6 |
type ISubject = interface procedure AdicionarObserver(Observer: IObserver); procedure RemoverObserver(Observer: IObserver); procedure Notificar; end; |
Classe Concrete Subject
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.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 |
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:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
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):
1 2 3 4 5 6 7 8 |
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?
Em açã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:
1 2 3 |
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:
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.
Conclusão
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:
Grande abraço, pessoal!
Boa noite André, muito esclarecedor seus artigos, exemplos fáceis de entender.
Parabéns e muito obrigado
Boa noite, Ricardo.
Em que agradeço pelo comentário! Grande abraço!
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!
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!
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!
Opa, muito obrigado, Cleo!
É muito gratificante ler os seus comentários. Agradeço por acompanhar a série de artigos!
Grande abraço!
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.
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!
Muito esclarecedor, direto e completo, estava com dúvidas de como implementar e ao ler o artigo minha cabeça se abriu de forma incrível. Obrigado por dedicar seu tempo fazendo o melhor para quem está querendo aprender novas formas de trabalhar.
Olá, Bhawan!
Fiquei muito feliz ao ler o seu comentário!
Essa foi a minha intenção com essa série de artigos: “descomplicar” o mundo dos padrões de projeto.
Obrigado! Abração!
Parabéns pelo seus artigos. Estou lendo todos. Abraço.
Muito obrigado, Rafael!
Espero que goste de toda a série.
Abraço!
Boa noite, André.
Nossa, foi uma luta para começar a entender esse padrão. Comecei a tentar faz dois dias, mas com sua explicação, ficou bem mais fácil, confesso que não consigo fazer sem olhar seu exemplo.
Dúvida: por que quando gero através do Create Pattern do próprio Delphi ele me gera algumas instruções com o sinal de @ na frente e o sinal ^ na frente?
Sugestão: Você poderia pegar esse exemplo e colocar outros padrões neles, por exemplo o Strategy, com uma regra “simples” por exemplo Débito e/ou Crédito tem que ser maior que zero, apenas para entendermos a aplicabilidade de dois padrões juntos.
Eu peguei dois exemplos do livro “Use a Cabeça Padrões de Projeto”, e consegui a duras penas desenvolver dois exemplos, sendo o padrão Strategy e depois juntei com o Factory, sendo esse último em Factory Method e Abstract Factory. Seria interessante aprender a juntá-los no Observer, por isso minha sugestão acima.
Grande abraço e obrigado pelo excelente artigo.
Olá, Ronaldo, boa noite!
Excelente pergunta. Algumas classes criadas automaticamente pelo Delphi trabalham com endereços de memória e ponteiros para referências de objetos e alocações dinâmicas. Trata-se de um conceito um pouco mais avançado do que estamos acostumados a praticar, porém, não se preocupe! Essas classes são apenas “esqueletos” para agilizar a implementação destes padrões. O desenvolvedor pode alterá-las e adaptá-las conforme a sua necessidade.
Agradeço a sua sugestão, Ronaldo! Em futuras publicações, vou procurar combinar padrões de projeto e exemplificar essa possibilidade. A propósito, Strategy e Factory é uma combinação clássica, rsrs.
Obrigado! Abraço!
No meu caso, não consigo ativar o ClientDataSet no Frame Pattern.Agrupamento. Diz que falta um Data Provider ou um Data Packet. Fiz exatamente igual ao sei exemplo.
Olá. Ígor, tudo bem?
Faça o download do projeto de exemplo neste link e compare com o seu código. Pelo que posso inferir, você precisar criar o DataSet em memória em design time.
Se mesmo assim não funcionar, envie um e-mail para [email protected] para que eu possa lhe ajudar melhor.
Abraço!