Observer Pattern - Design Patterns com Typescript

Observer Pattern - Design Patterns com Typescript

O Observer pattern é um dos padrões de projeto mais úteis, amplamente utilizado, especialmente no frontend com os novos frameworks/libraries reativas como Vue, React e Angular por exemplo, assim, entender bem seus conceitos e o problema que ele pretende resolver, é fundamental para que possamos usá-lo da melhor forma possível.

O que é o observer pattern afinal?

O Observer pattern é um padrão comportamental, assim como State, Iterator e Strategy (outros padrões de projetos que ainda vamos ver durante essa série de artigos), e como a maioria dos padrões desta categoria, trata da comunicação entre objetos.

Por definição, o padrão Observer define uma dependência um-para-muitos entre objetos, que ao mudar o estado de um objeto (Subject), todos os observadores (Observers) são notificados e atualizados automaticamente, sendo que de uma forma mais simplista, a ideia é fazer com que uma mudança em um objeto do interesse de muitos (Subject) ao ser modificado, notifique a todos os interessados (Observers) sobre essa mudança, transferindo seu novo estado.

Abaixo um diagrama que representa esse fluxo:

Como tenho tentado nessa série de artigos, vou fazer o possível para trazer neste um exemplo mais concreto, mais fácil de perceber o problema que o padrão resolve, e como podemos implementar ele, de uma forma bem simplificada, mas que já vai lhe dar uma boa base de seu funcionamento.

Para exemplificar, vamos usar um exemplo de um sistema de controle de temperatura, onde temos um sensor que lê a temperatura, e duas classes Fan (Ventilador) e TemperatureDisplay que dependem dessa informação do sensor para executar alguma ação, assim vamos implementar primeiro uma solução mais simplista para identificar o problema, depois implementamos o padrão Observer, bora ao código 😉.

Primeiro precisamos definir o sensor:

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
interface TemperatureSensor {
getTemperature(): Number;
}

class ArduinoTemperatureSensor implements TemperatureSensor {
protected temperature: Number = 0;

constructor() {
setInterval(this.setNewTemperature.bind(this), 2000);
}

public getTemperature(): Number {
return this.temperature;
}

protected setNewTemperature() {
const randomTemperature = Math.floor(Math.random() * 120);
console.info(`New Temperature: ${randomTemperature}`);
this.setTemperature(Math.floor(randomTemperature));
}

protected setTemperature(temperature: Number) {
this.temperature = temperature;
}
}

Uma implementação bem simples apenas para ilustrar, sendo que definimos um método “setNewTemperature” chamado a cada 2 segundos e que gera uma temperatura randômica para o sensor.

Agora precisamos criar as classes que vão usar esse sensor:

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
36
37
class Fan {
protected temperatureSensor: TemperatureSensor;
protected running: boolean = false;

constructor(temperatureSensor: TemperatureSensor) {
this.temperatureSensor = temperatureSensor;
setInterval(this.monitorTemperature.bind(this), 100);
}

public update(temperature: Number) {
console.info(`Fan read temperature ${temperature}`);
if (temperature < 50) {
return this.turnOff();
}

return this.turnOn();
}

protected monitorTemperature() {
const temperature = this.temperatureSensor.getTemperature();
this.update(temperature);
}

protected turnOn () {
if (!this.running) {
this.running = true;
console.info('Fun started');
}
}

protected turnOff () {
if (this.running) {
this.running = false;
console.info('Fun stoped');
}
}
}

A classe Fan simula um ventilador, recebendo um sensor, e sempre que a temperatura atingir um valor maior que 50º, o mesmo é acionado, e desligado sempre que a temperatura for menor que esse valor.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class TemperatureDisplay {
protected readonly temperatureSensor: TemperatureSensor;

constructor(temperatureSensor: TemperatureSensor) {
this.temperatureSensor = temperatureSensor;
setInterval(this.monitorTemperature.bind(this), 100);
}

public update (temperature: Number) {
console.info(`Display: ${temperature}`);
}

protected monitorTemperature() {
const temperature = this.temperatureSensor.getTemperature();
this.update(temperature);
}
}

A classe TemperatureDisplay é ainda mais simples, apenas imprime no console a temperatura aferida pelo sensor.

Assim poderíamos usar essas classes da seguinte forma:

1
2
3
const arduinoTemperatureSensor = new ArduinoTemperatureSensor();
const fan = new Fan(arduinoTemperatureSensor);
const temperatureDisplay = new TemperatureDisplay(arduinoTemperatureSensor);

Ao rodar esse código, alguns aspectos ficam muito claros:

  • Uma classe que utiliza o sensor, tem sua precisão diretamente dependente da frequência que consulta o sensor;
  • Fica claro também que se um número muito grande de objetos dependerem do sensor, teremos uma sobrecarga no sensor, talvez até atrapalhando função principal que seria capturar a temperatura;
  • Para cada nova classe que precisa dos dados do sensor vamos precisar implementar uma nova lógica para garantir a precisão necessária para sua atualização;

Assim, o padrão Observer, entra em cena exatamente para resolver esses e outros problemas introduzidos pela abordagem anterior.

Implementando o Observer Pattern

Para implementação do padrão precisamos definir duas interfaces, Observer e Subject.

1
2
3
4
5
6
7
8
9
interface Observer {
notify(temperature: Number): void;
}

interface Subject {
registerObserver(observer: Observer): void;
unregisterObserver(observer: Observer): void;
notifyObservers(): void;
}

Vamos agora estender nossa interface de TemperatureSensor para que possa se comportar como um Subject:

1
2
3
interface TemperatureSensor extends Subject {
getTemperature(): Number;
}

E alterar nosso sensor para essa nova interface:

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
class ArduinoTemperatureSensor implements TemperatureSensor {
protected temperature: Number = 0;
protected observers: Observer[] = [];

constructor() {
setInterval(this.setNewTemperature.bind(this), 2000);
}

public registerObserver(observer: Observer): void {
this.observers.push(observer);
}

public unregisterObserver(observer: Observer): void {
this.observers = this.observers.filter(o => o !== observer )
}

public notifyObservers(): void {
this.observers.forEach((observer) => observer.notify(this.getTemperature()));
}

public getTemperature(): Number {
return this.temperature;
}

protected setNewTemperature() {
const randomTemperature = Math.floor(Math.random() * 120);
console.info(`New Temperature: ${randomTemperature}`);
this.setTemperature(Math.floor(randomTemperature));
}

protected setTemperature(temperature: Number) {
this.temperature = temperature;
this.notifyObservers();
}
}

Reparem agora como a implementação das classes que dependem do sensor fica muito mais simplificada e precisa:

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
class Fan implements Observer {
protected temperatureSubject: Subject;
protected running: boolean = false;

constructor(temperatureSubject: Subject) {
this.temperatureSubject = temperatureSubject;
this.temperatureSubject.registerObserver(this);
}

public notify(temperature: Number) {
console.info(`Fan read temperature ${temperature}`);
if (temperature < 50) {
return this.turnOff();
}

return this.turnOn();
}

protected turnOn () {
if (!this.running) {
this.running = true;
console.info('Fan started');
}
}

protected turnOff () {
if (this.running) {
this.running = false;
console.info('Fan stoped');
}
}
}
1
2
3
4
5
6
7
8
9
10
11
12
class TemperatureDisplay implements Observer {
protected readonly temperatureSubject: Subject;

constructor(temperatureSubject: Subject) {
this.temperatureSubject = temperatureSubject;
this.temperatureSubject.registerObserver(this);
}

public notify(temperature: Number) {
console.info(`Display: ${temperature}`);
}
}

Podemos assim executar como antes:

1
2
3
const arduinoTemperatureSensor = new ArduinoTemperatureSensor();
const fan = new Fan(arduinoTemperatureSensor);
const temperatureDisplay = new TemperatureDisplay(arduinoTemperatureSensor);

Como puderam perceber com essa mudança de abordagem, ganhamos uma precisão muito superior, fazendo com que todos objetos que dependam dos dados do sensor sejam atualizado imediatamente após uma nova leitura do sensor, além disso temos algumas outras vantagens, como por exemplo a simplificação das classes que precisam dos dados do sensor e principalmente removemos o acoplamento entre os objetos, fazendo com que interajam normalmente, porém conhecendo muito pouco um do outro.

O Código completo para esta implementação pode ser encontrado em: https://github.com/meneguite/typescript-design-patterns/blob/master/observer/index.ts

Outros padrões de Projetos

Comentários

Your browser is out-of-date!

Update your browser to view this website correctly. Update my browser now

×