Padrão de Projeto: Observer
(13041) (3)
CategoriasAndroid, Design, Protótipo
AutorVinÃcius Thiengo
VÃdeo aulas186
Tempo15 horas
ExercÃciosSim
CertificadoSim
CategoriaDesenvolvimento Web
Autor(es)Robert C. Martin
EditoraAlta Books
Edição1ª
Ano2023
Páginas416
Tudo bem?
Neste artigo vamos falar do padrão de projeto Observer.
Além de entende-lo ele será útil para o entendimento de ao menos um método de refatoração, mais precisamente o método Substituir Notificações Hard-Coded Por Observer.
Antes de prosseguir, não esqueça de se inscrever na 📫 lista de e-mails do Blog para receber em primeira mão todos os conteúdos exclusivos sobre desenvolvimento e codificação limpa.
Abaixo os tópicos abordados em artigo:
Apresentação
O padrão de projeto Observer é bem simples e muito utilizado.
O Java, por exemplo, tem entidades que nos permitem implementar esse padrão abstraindo em muito a codificação que seria necessária sem a utilização dessas entidades.
O Observer é útil quando precisamos que dois ou mais objetos "escutem" a determinados eventos em um outro objeto.
Os objetos que estão escutando são conhecidos como Observers e o objeto que é escutado (ou observado) é conhecido como Subject.
Vamos iniciar com a definição formal do padrão:
Permite que um objeto, observado, notifique automaticamente todos os objetos vinculados a ele (objetos observadores) respeitando a relação um-para-muitos. A notificação ocorre assim que o estado do objeto observado é atualizado.
Diagrama
O método removeObserver() é opcional, se em seu código nunca for necessário a remoção de um observador, não o implemente. Caso contrário esse método se tornará código morto.
Para melhor entendimento do padrão vamos ao exemplo em código.
Código de exemplo
Temos uma estação meteorológica que é responsável por fornecer dados de temperatura, umidade e pressão.
Temos também outras três entidades que necessitam dos dados atualizados dessa estação, ou seja, a cada nova atualização na estação essas entidades precisam ser informadas.
O que essas entidades fazem?
Em nosso exemplo elas serão responsáveis por apresentar na tela, dos usuários, os resultados de cálculos com esses dados meteorológicos.
As entidades são:
- EstatisticasDisplay;
- MediaDisplay;
- PressaoAtmosfericaDisplay.
E, obviamente, a estação meteorológica: EstacaoMeteorologica.
Segue os códigos.
Começando pela classe EstacaoMeteorologica:
public class EstacaoMeteorologica {
private float temperatura;
private float umidade;
private float pressao;
public void setMedicoes(
float temperatura,
float umidade,
float pressao ){
this.temperatura = temperatura;
this.umidade = umidade;
this.pressao = pressao;
}
}
Então a classe EstatisticasDisplay:
public class EstatisticasDisplay {
private float temperatura;
private float umidade;
private float pressao;
private void display(){
System.out.println( "Temperatura: " + temperatura );
System.out.println( "Umidade: " + umidade );
System.out.println( "Pressao: " + pressao );
}
}
A classe MediaDisplay:
public class MediaDisplay {
private Subject subject;
private float temperaturaMin, temperaturaMax, temperaturaMed;
private float umidadeMin, umidadeMax, umidadeMed;
private float pressaoMin, pressaoMax, pressaoMed;
public void display(){
System.out.println( "Temperatura média: " + temperaturaMed );
System.out.println( "Umidade média: " + umidadeMed );
System.out.println( "Pressão média: " + pressaoMed );
}
}
E por fim a classe PressaoAtmosfericaDisplay:
public class PressaoAtmosfericaDisplay {
private float temperatura;
private float pressao;
public void display(){
System.out.println( "Pressão atmosférica: " + calcPressaoAtmosferica() );
}
...
}
E agora, como informar as entidades de display sobre as atualização em EstacaoMeteorologica?
Uma possível solução seria colocar dentro de EstacaoMeteorologica variáveis de instância de cada um dos tipos de display.
Mas nesse caso estaríamos violando um dos princípios de padrões de projeto: "Programe para interface e não para implementação."
Ok, eu sei que de vez em quando violar alguns princípios é necessário, porém note que se mais entidades que precisam dos dados de EstacaoMeteorologica forem adicionadas, nos vamos ter de modificar essa classe, trabalhando no modelo "alteração" ao invés de "adição", algo que é pouco produtivo para a evolução do sistema.
Lembrando que utilizar um atributo para cada um dos displays nos forçaria a ter códigos específicos (hard-coded) para cada uma dessas entidades, pois não temos herança e compartilhamento de Interfaces no projeto, deixando os códigos ainda mais acoplados uns aos outros.
Ok, e que tal trabalharmos com herança? Dessa forma poderíamos ter uma lista de apenas um tipo e consequentemente apenas uma interface de comunicação.
É, seria uma opção. Alias isso seria algo muito similar ao padrão Observer, somente a herança entre as classes observadoras que estaria violando a aplicação desse padrão.
E outra, o trabalho com herança pode ser muito mais ineficiente quando podemos utilizar Interface (estrutura da linguagem) para estender as classes de nosso algoritmo.
E quais seriam os pontos negativos de trabalhar com herança nesse caso?
Primeiro por que herança é um recurso escasso, somente podemos ter uma herança direta (na maioria das linguagens). Segundo porque herança entre classes não relacionadas quebra o polimorfismo e consequentemente a boa leitura do código.
Ei, espera um pouco, as classes de display citadas anteriormente são todas relacionadas, não são?
Sim, são.
Mas e se fosse necessária a evolução do sistema (isso vai acontecer), por exemplo, uma empresa de telemetria passasse a ser cliente dessa estação meteorológica.
A classe dessa empresa teria de herdar também de uma classe que provavelmente pouca faria sentido a ela: XYZEmpresaTelemetria extends Display. No deal!
Provavelmente há mais problemas, mas esses já são o suficiente para não optarmos as soluções alternativas ao padrão Observer, alias essa será a solução, aplicar o Observer.
Nesse exemplo a classe EstacaoMeteorologica será a classe Subject do padrão, a responsável por conter as referências das instâncias observadoras e então comunicá-las sempre que houver mudança em seu estado.
O que é mudança de estado em um objeto?
É literalmente quando algum de seus atributos é atualizado, somente isso. Grave que estado de um objeto é relacionado a atributos, comportamento é relacionado a métodos.
Voltando ao código de exemplo...
... nossas entidades de display, incluindo as possíveis empresas de telemetria, futuras clientes, seriam todas elas as classes observadoras no padrão proposto.
Abaixo as duas novas Interfaces, Subject e Observer:
public interface Subject {
public void addObserver( Observer observer );
public void removeObserver( Observer observer );
public void notifyObservers();
}
public interface Observer {
public void update(
float temperatura,
float umidade,
float pressao );
}
Note que você encontrará em alguns exemplos na Web a entidade Subject sendo implementada como classe, algo que é possível mesmo com os problemas já citados anteriormente, porém o Observer deve ser uma Interface.
Ao menos essa parte do padrão, Observer como Interface, recomendo que seja seguido a risca mesmo quando adaptações são necessárias.
Implementando essas Interfaces nas classes apresentadas anteriormente, teríamos os seguintes códigos.
Começando pela EstacaoMeteorologica:
public class EstacaoMeteorologica implements Subject {
private List<Observer> observers; /* NOSSAS ENTIDADES OBSERVADORAS ESTÃO AQUI */
private float temperatura;
private float umidade;
private float pressao;
public EstacaoMetereologica(){
observers = new ArrayList<>();
}
public void setMedicoes(
float temperatura,
float umidade,
float pressao ){
this.temperatura = temperatura;
this.umidade = umidade;
this.pressao = pressao;
notifyObservers();
}
@Override
public void addObserver( Observer observer ){
observers.add( observer );
}
@Override
public void removeObserver( Observer observer ){
int index = observers.indexOf( observer );
if( index > -1 ){
observers.remove( observer );
}
}
@Override
public void notifyObservers(){ /* MÉTODO ONDE HÁ A COMUNICAÇÃO DA MUDANÇA DE ESTADO */
for( Observer o :observers ){
o.update( temperatura, umidade, pressao );
}
}
}
Nessa parte é importante que você entenda o código acima.
O List de observers está ali para que possamos ter mais do que apenas uma entidade observadora da mudança de estado em EstacaoMeteorologica.
Os métodos notifyObservers(), addObserver() e removeObserver() são auto-comentados, ou seja, notificar, adicionar e remover.
O construtor para inicializar nossa lista de observers é algo opcional, poderíamos ter inicializado a lista logo na declaração dela, na classe.
A chamada a notifyObservers() logo no final do método que atualiza os dados, o método setMedicoes(), é essencial para que a funcionalidade de notificação, que é objetivo do padrão Observer, funcione.
Com isso podemos prosseguir com as outras classes, agora as classes observadoras.
Segue classe EstatisticasDisplay:
public class EstatisticasDisplay implements Observer {
private Subject subject;
...
public EstatisticasDisplay( Subject subject ){
this.subject = subject;
this.subject.addObserver( this );
}
private void display(){
...
}
@Override
public void update(
float temperatura,
float umidade,
float pressao ) {
this.temperatura = temperatura;
this.umidade = umidade;
this.pressao = pressao;
display();
}
}
Para evitar a sobrecarga de informação vamos omitir algumas partes já apresentadas das classes.
Note que em nosso caso optamos por ter uma referência a uma implementação de Subject, isso para podermos adicionar e remover observers de maneira simples.
O método update() é o que receberá os dados atualizados de nossa implementação de Subject, nesse caso, uma instância de EstacaoMeteorologica.
Agora segue outra classe observadora, MediaDisplay:
public class MediaDisplay implements Observer {
private Subject subject;
...
public MediaDisplay( Subject subject ){
this.subject = subject;
this.subject.addObserver( this );
}
public void display(){
...
}
@Override
public void update(
float temperatura,
float umidade,
float pressao ){
mediaTemperatura( temperatura );
mediaUmidade( umidade );
mediaPressao( pressao );
display();
}
...
}
A explicarão do novo código em MediaDisplay é a mesma da classe observadora anterior.
Logo, prosseguimos para a classe observadora PressaoAtmosfericaDisplay:
public class PressaoAtmosfericaDisplay implements Observer {
private Subject subject;
...
public PressaoAtmosfericaDisplay( Subject subject ){
this.subject = subject;
this.subject.addObserver( this );
}
public void display(){
...
}
@Override
public void update(
float temperatura,
float umidade,
float pressao ){
this.temperatura = temperatura;
this.pressao = pressao;
display();
}
}
Com isso temos as classes observadoras ouvindo as atualizações que ocorrem em Subject, aqui a EstacaoMeteorologica.
Consequentemente o padrão observer está implementado.
Note que se tivermos clientes novos, digo, empresas de telemetria, por exemplo.
Com essa implementação de Interface as classes dessas empresas de telemetria somente precisarão aplicar a Interface Observer para poderem se tornar observadoras da classe Subject, EstacaoMeteorologica.
Agora vamos a um exemplo de código cliente das classes que implementam o padrão Observer:
public class TesteEstacao {
public static void main( String[] args ){
EstacaoMeteorologica estacao = new EstacaoMeteorologica();
EstatisticasDisplay estatisticasDisplay = new EstatisticasDisplay( estacao );
MediaDisplay mediaDisplay = new MediaDisplay( estacao );
PressaoAtmosfericaDisplay pressaoAtmosfericaDisplay = new PressaoAtmosfericaDisplay( estacao );
estacao.setMedicoes( 87, 47, 14.4f );
estacao.setMedicoes( 91, 23, 108.2f );
estacao.setMedicoes( 65, 30, 41.2f );
}
}
Ok, acho que entendi, mas não vi o por quê da classe PressaoAtmosfericaDisplay ter de receber também o dado de umidade, sendo que ela não precisa dele. Por que isso?
Você está certo, o nome do tipo de implementação do padrão Observer que aplicamos até aqui é "empurrar", ou seja, a classe que implementa o Subject simplesmente envia todos os dados para as instâncias de classes observadoras registradas, mesmo aqueles dados que não sofreram alteração ou não são úteis a algumas dessas instâncias.
Então existe alguma outra maneira de trabalhar o Observer, certo?
Sim, a versão "puxar".
Lembra que no início do artigo informei sobre as entidades nativas do Java para implementação do padrão Observer?
Então, essas são as que nos permitem implementar o padrão Observer no modo "puxar" da maneira mais simples possível.
Agora vamos atualizar nosso código para trabalhar com essas entidades, a classe Observable (nossa Subject) e a Interface Observer.
A classe EstacaoMeteorologica ficaria da seguinte forma:
public class EstacaoMeteorologica extends Observable {
private float temperatura;
private float umidade;
private float pressao;
public void setMedicoes(
float temperatura,
float umidade,
float pressao ){
this.temperatura = temperatura;
this.umidade = umidade;
this.pressao = pressao;
setChanged();
notifyObservers();
}
public float getTemperatura(){
return temperatura;
}
public float getUmidade(){
return umidade;
}
public float getPressao(){
return pressao;
}
}
Muita coisa mudou, não?!
Isso mesmo, nossa classe pai Observable já faz o trabalho pesado para nós:
- Adicionar;
- Remover;
- Notificar;
- Gerenciar lista de observers;
- ...
Isso é tudo tarefa dela, somente temos que invocar os métodos corretos.
Note que também adicionamos uma chamada a setChanged().
O que esse método faz faz?
Ele atualiza um sinalizador (a velha e conhecida, flag) informando que caso o método notifyObservers() seja invocado, os observers registrados podem ser notificados sobre a atualização.
Caso você não chame o setChanged() as notificações não ocorreram, mesmo invocando o notifyObservers().
Por que isso? Não seria mais simples somente invocar notifyObservers()?
Provavelmente sim, porém a chamada a esse método previne nosso código de não manter atualizando as instâncias observadoras para qualquer entrada, mesmo aquelas que oscilam de maneira pouco utilizável pelos observers.
É uma forma de manter o código mais eficiente.
Caso a atualização para qualquer entrada seja necessária, coloque o setChanged() como fizemos acima, logo antes de notifyObservers().
Caso contrário você pode colocar esse método em um trecho específico que define se a entrada de dados atual é ou não uma entrada digna de atualização nas instâncias observadoras.
Ok, e os métodos getters, por que o uso deles? E cadê os setters?
Os métodos getters vão permitir que as instâncias de classes observadoras "puxem" somente os dados que as interessam.
A não implementação dos setters é para evitar código morto, não precisaremos deles aqui.
Ok, vamos continuar com as atualizações.
Seguindo para a classe EstatisticasDisplay:
public class EstatisticasDisplay implements Observer {
private Observable subject;
...
public EstatisticasDisplay( Observable subject ){
this.subject = subject;
this.subject.addObserver( this );
}
private void display(){
...
}
@Override
public void update(
Observable observable,
Object data ){
EstacaoMeteorologica estacao = (EstacaoMeteorologica) observable;
this.temperatura = estacao.getTemperatura();
this.umidade = estacao.getUmidade();
this.pressao = estacao.getPressao();
display();
}
}
Note que a variável de instância subject tem um novo tipo, Observable.
A Interface implementada é a Observer, do pacote java.util, alias Observable também é desse pacote.
E a grande mudança, os parâmetros de entrada do método update().
O primeiro é nada mais nada menos que nossa instância de classe observada, o Observable, ou, aqui, a classe EstacaoMeteorologica.
O segundo é um possível objeto que poderíamos estar passando como argumento de notifyObservers() lá em EstacaoMeteorologica, em nosso caso não utilizamos essa sobrecarga.
O parâmetro de entrada observable em update() é a exata mesma instância referenciada na variável subject das classes observadoras.
De acordo com a lógica de negócio que estamos utilizando aqui, a variável subject nos ajuda a registrar a instância observadora, a variável local observable nos ajuda a obter os novos valores atualizados.
Continuando com a atualização das outras classes observadoras.
Agora a MediaDisplay:
public class MediaDisplay implements Observer {
private Observable subject;
...
public MediaDisplay( Observable subject ){
this.subject = subject;
this.subject.addObserver( this );
}
public void display(){
...
}
@Override
public void update(
Observable observable,
Object data ){
EstacaoMeteorologica estacao = (EstacaoMeteorologica) observable;
mediaTemperatura( estacao.getTemperatura() );
mediaUmidade( estacao.getUmidade() );
mediaPressao( estacao.getPressao() );
display();
}
...
}
E então a implementação da classe observadora PressaoAtmosfericaDisplay:
public class PressaoAtmosfericaDisplay implements Observer {
private Observable subject;
...
public PressaoAtmosfericaDisplay( Observable subject ){
this.subject = subject;
this.subject.addObserver( this );
}
public void display(){
...
}
@Override
public void update(
Observable observable,
Object data ){
EstacaoMetereologica estacao = (EstacaoMetereologica) observable;
this.temperatura = estacao.getTemperatura();
this.pressao = estacao.getPressao();
display();
}
...
}
Então a classe, do código anterior, que obtia dados desnecessários, mais precisamente o dado umidade, foi otimizada, agora trabalhando também com a versão "puxar" do padrão Observer.
Com isso podemos seguir para o novo código cliente:
public class Teste {
public static void main( String[] args ) {
EstacaoMeteorologica estacao = new EstacaoMeteorologica();
EstatisticasDisplay estatisticasDisplay = new EstatisticasDisplay( estacao );
MediaDisplay mediaDisplay = new MediaDisplay( estacao );
PressaoAtmosfericaDisplay pressaoAtmosfericaDisplay = new PressaoAtmosfericaDisplay( estacao );
estacao.setMedicoes( 87, 47, 14.4f );
estacao.setMedicoes( 91, 23, 108.2f );
estacao.setMedicoes( 65, 30, 41.2f );
}
}
É isso mesmo. Exatamente o mesmo código.
Note que se não estiver utilizando a linguagem Java é tranquilamente possível adaptar nossa versão "empurrar" do padrão Observer para trabalhar na versão "puxar".
Pontos negativos
- Se a implementação for encadeada, por exemplo: um Observer é também um Subject (algo possível). Se esse tipo de encadeamento acontecer de forma descontrolada o vazamento de memória será um provável problema. Mais precisamente, OutOfMemoryException;
- Outra problemática é quando seu código tem somente um observador e um sujeito, em qualquer circunstância. Implementar o Observer nesse cenário é tornar o código inflado com a aplicação desnecessária de um padrão.
Pontos positivos
- Como com todos os outros padrões de projeto, o Observer aplica uma linguagem universal ao projeto, permitindo a fácil leitura e compreensão por parte de outros programadores também participantes do desenvolvimento;
- O código trabalha com notificação via composição ao invés de implementação, o que permite a evolução mais eficiente do projeto, além do número de instâncias observadoras poder ser atualizado de forma dinâmica.
Conclusão
O padrão Observer é uma excelente opção para códigos como o do exemplo apresentado ou códigos com listeners de eventos em geral.
Um objeto tendo de notificar mais do que um outro objeto, nesse caso o Observer é o código a ser implementado. Além do mais, é um padrão simples de entender e utilizar.
Quando implementando o padrão, busque a maneira mais otimizada, ou seja, como apresentado no artigo, a versão "puxar" tende a ser mais eficiente.
Verifique também se a linguagem de programação que você está utilizando, se ela já não fornece entidades nativas para adiantar o trabalho de implementação.
Lembrando que entidades nativas muito provavelmente vão ser melhores que qualquer de suas implementações, isso devido a série de testes e evoluções que essas entidades passam.
Então é isso.
Parabéns por ter concluído o artigo. São poucos os desenvovledores que investem tempo no estudo de padrões de codificação e código limpo.
Por fim, não deixe de se inscrever na 📩 lista de e-mails do Blog para receber os conteúdos de desenvolvimento e codificação limpa exclusivos... e em primeira mão.
Abraço.
Fontes
Use a Cabeça! Padrões de Projetos
Comentários Facebook