Desvendando o Padrão Observer: Uma jornada pelas notificações de mudança de estado

O Observer é um design pattern do tipo comportamental, onde a alteração do estado de um objeto influencia em outros, basicamente é uma relação de um para muitos, onde qualquer alteração no objeto observado pode fazer com que os seus observadores alterem o seu estado também.

Nesse padrão basicamente temos dois papéis, o observado e os observadores, onde o observado geralmente possui um estado, que ao ser alterado é notificado aos observadores que esse estado foi alterado, com isso cada observador pode reagir a mudança.

Esse padrão promove um desacoplamento entre os papéis, esse padrão também é altamente escalável permitindo adicionar muitos observadores mesmo ao longo de implementações futuras, porque mantém uma baixa dependência entre os objetos, e com isso temos um sistema mais flexível e extensível.

Implementação

Vamos implementar esse padrão, para isso vamos definir uma interface que irá definir os métodos necessários para subscrição dos observadores, remoção e notificação toda vez que o estado do nosso observado for alterado.

public interface Observado {

	void adicionarObservador(Observador observador);
	
	void removerObservador(Observador observador);
	
	void notificarObservadores();
	
}
Observado.java

Agora precisamos de uma interface comum para os nossos observadores, isso é importante porque será o contrato que definirá qual é o método chamado quando eles forem notificados.

public interface Observador {

	void atualização(Observado observado);

}
Observador.java

Pronto agora todas as nossas interfaces estão criadas, vamos de fato implementar um objeto que será observado, e nesse ponto é bem simples, basicamente precisamos implementar a interface Observado e adicionar uma variável para eu guardar meus observadores.

public class ObservadoConcreto implements Observado {

	// Variável responsável por guardar os observadores
	private List<Observador> observadores = new ArrayList<Observador>();
	
	// Estado do meu observado
	private int estado;
	
	@Override
	public void adicionarObservador(Observador observador) {
		observadores.add(observador);
	}

	@Override
	public void removerObservador(Observador observador) {
		observadores.remove(observador);
	}

	@Override
	public void notificarObservadores() {
		for(Observador observador : observadores) {
			observador.atualizacao(this);
		}
	}

  // Métodos responsáveis pela alteração do estado	
	public void setEstado(int estado) {
		this.estado = estado;
		notificarObservadores();
	}
	
	public int getEstado() {
		return this.estado;
	}

}
ObservadoConcreto.java

Note que a implementação é bem simples, adicionamos uma lista para guardar nosso observadores, nos métodos de adição e remoção estamos utilizando os métodos disponibilizados no próprio List.

No método principal que é o nosso “notificarObservadores”, também é bem simples precisamos apenas percorrer nossa lista que contém nossos observadores e utilizar o método atualizar definido na interface, passando o nosso próprio observado, para que na outra ponto o observador tenha acesso aos dados exposto pelo nosso observado.

Agora precisamos implementar nossos observadores, que acaba sendo uma tarefa bem simples, vamos apenas implementar a interface e sobrescrever o método, segue o exemplo:

Observador 1

public class Observador1 implements Observador {

	@Override
	public void atualizacao(Observado observado) {
		ObservadoConcreto observadoConcreto = (ObservadoConcreto) observado;
		System.out.println(String.format("Observador 1: %d", observadoConcreto.getEstado()));
	}

}
Observador1.java

Observador 2

public class Observador2 implements Observador {

	@Override
	public void atualizacao(Observado observado) {
		ObservadoConcreto observadoConcreto = (ObservadoConcreto) observado;
		System.out.println(String.format("Observador 2: %d", observadoConcreto.getEstado()));
	}

}
Observador2.java

Testando a implementação

Agora de fato vamos instanciar nossos objetos e validar o funcionamento, para isso criamos uma classe com o método main, e vamos capturar os valores do terminal.

public class ExemploObserver {

	public static void main(String[] args) {

	
		// Instanciando o observado
		ObservadoConcreto observado = new ObservadoConcreto();

		// Adicionando os observadores
		observado.adicionarObservador(new Observador1());
		observado.adicionarObservador(new Observador2());
		
		// Criar um scanner para caputurar valores do terminal
		Scanner scanner = new Scanner(System.in);
		
		int numero = 0;
		
		while(numero != -1) {
			System.out.print("Digite um número inteiro: ");
			numero = scanner.nextInt();
			
			// Vamos fazer a mudança de estado do nosso observado
			observado.setEstado(numero);
		}

		// fechar o scanner
		scanner.close();
	}
}
ExemploObserver.java

Testando a nossa implementação perceba que toda vez que digitamos um valor no terminal o estado do nosso observado é alterado e por sua vez ele notifica os nossos observadores que por sua vez reagem a essa alteração.

Implementação nativa do Java

O Java já nos disponibiliza interfaces que facilitam essas implementações, mas elas foram marcadas como Deprecated a partir da versão 9, isso significa que embora elas estejam ainda presentes, podem ser removidas em versões futuras, e a orientação é que nós mesmos implementamos nossas próprias soluções de observable, para título de curiosidade, vamos demonstrar como seria utilizando as interfaces disponibilizadas pelo Java.

Observado

public class Observado extends Observable {

	private int estado;

	public int getEstado() {
		return estado;
	}

	public void setEstado(int estado) {
		this.estado = estado;
		setChanged();
		notifyObservers(this);
	}
	
}
Observado.java

Observadores

public class Observador1 implements Observer {

	@Override
	public void update(Observable observable, Object arg1) {
		Observado observado = (Observado) observable;
		System.out.println("Observador 1 " + observado.getEstado());
	}
	
}
Observador1.java
public class Observador2 implements Observer {

	@Override
	public void update(Observable observable, Object arg1) {
		Observado observado = (Observado) observable;
		System.out.println("Observador 2 " + observado.getEstado());
	}
	
}
Observador2.java

E o funcionamento fica basicamente o mesmo toda vez que eu realizar uma alteração de estado do nosso observado, nossos observadores serão notificados através do método update.

Observações sobre o Observer

Embora esse seja um excelente padrão para desacoplar funcionalidades dentro da sua aplicação é importante observar que alguns problemas podem ser ocasionados quando se adota esse pattern, vamos listar alguns exemplos:

  • Acoplamento indireto: Embora essa solução tenha o que chamamos de acoplamento mais fraco, de certa forma há um acoplamento indireto entre observado e observadores, isso pode tornar o código mais difícil de ser interpretado, adicionando complexidade a sua aplicação.
  • Atualizações não controladas: O fato dos observadores sempre receberem as notificações de toda alteração de estado, esse fato pode exigir que você implemente estratégias adicionais para controlar melhor as reações dos observadores, um exemplo desse tipo de solução é a adição de tópicos. Além do fato de não poder controlar de fato quem vai receber o que é possível que você acabe com atualizações excessivas a depender do nível de atualização do estado do seu observador, isso pode ocasionar um processamento exagerado e consumir muitos recursos como memória.
  • b Esse padrão não prevê uma ordem para as notificações, a depender do ambiente em que sua aplicação está sendo executada, pode acontecer de as notificações acontecerem em ordem diferente, fazendo com que observadores recebam valores diferentes.
  • Múltiplos observadores: Como a comunicação é feita a partir de uma interface, pode ser um problema caso você tenha observadores que precisem ser notificados de uma forma específica.
  • Notificações síncronas e assíncronas: É possível implementar esse padrão das duas formas, mas de forma assíncrona pode ser um problema na sincronização da aplicação, é sempre bom avaliar com calma a estratégia a ser utilizada para não ocasionar problemas futuros.
  • Gestão de observadores: Lidar com a gestão de observadores é um ponto bastante crítico nessa implementação, pois se não for feita de forma eficiente pode haver vazamento de memória consumindo recursos exagerados do sistema, impedindo a aplicação de executar de forma eficiente.

Conclusão

Esse padrão de fato é bem poderoso, podemos facilmente ir adicionando observadores que reagiram a mudanças de estado de forma desacoplada, lembrando que desacoplar as soluções é sempre uma boa prática, principalmente para implementações futuras e manutenção. Você ainda pode optar por transformar o nosso Observador em uma interface funcional, isso ajuda a reduzir bastante o código além de poder utilizar “generics” para caso o estado do seu observado seja complexo.

Links uteis

Mauricio Lima
Mauricio Lima

Bacharel em Ciência da Computação, profissional dedicado ao desenvolvimento de software e entusiasta da tecnologia.

Artigos: 65