Observer Pattern com exemplos no TypeScript!
Um design que não leva em consideração as mudanças corre o risco de uma grande reformulação no futuro. - Erich Gamma
O padrão Observer é um dos padrões mais populares. Vamos conhecer melhor ele fora de contexto de bibliotecas como RxJS. Apenas estudando em si o pattern com exemplos em Typescript. Mas antes vamos entender melhor o que são patterns e suas categorias.
O que é um padrão de projeto?
São soluções gerais e reutilizáveis para problemas que ocorrem no cotidiano em diferentes ambientes. Na Engenharia de Software, eles ajudam a escrever códigos melhores com mais rapidez. O uso dessas soluções gerais pode economizar tempo e trabalho porque reutilizamos o conhecimento e a experiência acumulados de muitos outros desenvolvedores. Existem três tipos de categorias de patterns, o comportamental, estrutural e criacional. Podemos definir sendo:
- Padrões de criação dizem respeito ao processo de criação de objetos.
- Padrões estruturais lidam com a composição de classes ou objetos.
- Padrões comportamentais caracterizam as maneiras pelas quais classes ou objetos interagem e distribuem responsabilidades.
As descrições acima foram retiradas do livro Padrões de Design: Elementos de Software Orientado a Objetos Reutilizáveis
Com essa breve introdução podemos mergulhar no Pattern central deste artigo.
Pattern Observer
O Observer é classificado na categoria de padrões comportamentais. Considerando este fato, seu propósito deve estar em caracterizar como classes ou objetos interagem e distribuem responsabilidade.
O pattern observer é um padrão de projeto de software no qual um objeto, chamado sujeito (Subject), mantém uma lista de seus dependentes, chamados observadores (observers), e os notifica automaticamente sobre qualquer mudança de estado, geralmente chamando um de seus métodos.
E qual é seu objetivo? A finalidade do padrão Observer é definir uma dependência um-para-muitos entre um objeto que notifica automaticamente todos os seus ouvintes sobre qualquer alteração de estado.
Mas quando podemos ou devemos usar? Você deve usar o padrão Observer sempre que houver um relacionamento um-para-muitos entre objetos e quando a alteração de um objeto exigir a alteração de outros. Além disso, você não sabe quantos objetos precisam ser alterados. Esse acoplamento fraco permite que o objeto notifique outros objetos sem fazer suposições sobre quem são esses objetos.
Use o padrão Observer quando as alterações no estado de um objeto podem exigir a alteração de outros objetos e o conjunto real de objetos é desconhecido de antemão ou muda dinamicamente. -refactoring.guru
Se as explicações de como este padrão comportamental age não ficaram claras, não se preocupe, vamos trazer exemplos da vida real para auxiliar na explicação.
Analogia
Imagine que você faz as assinaturas de jornais e revistas todo o mês. Quais são as etapas até eles chegarem até você? Veja:
- Um jornal começa a ser publicado.
- Você assina. Toda vez que há uma nova edição, ela é entregue para você.
- Você pode cancelar a inscrição para não obter mais edições.
- A editora continua publicando para outras pessoas que ainda podem assinar e cancelar a assinatura do jornal quando quiserem.
O Observer funciona da mesma maneira.
O mesmo se aplica aos artigos aqui no Hashnode. Eu sou o escritor - o assunto (Subject). Todos os que se inscrevem em uma newsletter do meu blog são observadores, pois assim que eu postar um novo artigo serão notificados. Se eu postar apenas bobagens por algum motivo, pode acontecer que muitos cancelem a inscrição para parar de receber notificações.
Podemos até mesmo fazer uma comparação com o Twitter. A plataforma fornece uma interface para assinar e cancelar assinaturas, que seria o mesmo que seguir uma pessoa. Um autor específico, como Elon Musk, é um CONCRETESUBJECT. Todos os usuários da plataforma que seguem o autor Musk, são observadores de seus Tweets. Uma assinatura específica de um autor específico é um CONCRETEOBSERVER .
A partir do diagrama acima podemos definir uma estrutura de participantes:
Subject: Conhece seus observadores. Fornece interface para assinar e cancelar a assinatura de objetos Observer. Cada sujeito pode ter muitos observadores.
Observer: Define uma interface de atualização para objetos que são notificados sobre alterações em um assunto.
ConcreteSubject: Envia uma notificação para seus observadores sempre que o estado muda.
ConcreteObserver: Referência a um ConcreteSubject. Implementa a interface de atualização do Observer para reagir às alterações de estado.
Vamos para um exemplo em código.
Playground
Você pode utilizar TypeScript Playground para testar o código abaixo:
interface Tweets {
post: string;
}
interface Subject<T> {
subscribe(observer: Observer<T>): void;
unsubscribe(observer: Observer<T>): void;
notify(data: T): void;
}
interface Observer<T> {
update(subject: Subject<T>, data: T): void;
}
interface ConcreteTwitterObserver extends Observer<Tweets> {
username: string;
};
interface ConcreteTwitterSubject extends Subject<Tweets> {
username: string;
};
class TwitterUserSubject implements ConcreteTwitterSubject {
constructor(public username: string) { }
private observers: ConcreteTwitterObserver[] = [];
public subscribe(observer: ConcreteTwitterObserver): void {
console.log(`${observer.username} subscribed to ${this.username} tweets.`);
const isAlreadyObserver = this.observers.includes(observer);
if (isAlreadyObserver) return;
this.observers.push(observer);
this.logSubscribers();
}
public unsubscribe(observer: ConcreteTwitterObserver): void {
console.log(`${observer.username} unsubscribed from ${this.username} tweets.`);
this.observers = this.observers.filter((other) => other !== observer);
this.logSubscribers();
}
public notify(tweets: Tweets): void {
console.log(`${this.username} notifies its ${this.observers.length} subscribers of the new tweet.`);
this.observers.map(observer => observer.update(this, tweets));
}
public publishNewTweet(tweets: Tweets): void {
console.log(`${this.username} published a new tweet: '${tweets.post}'`);
this.notify(tweets);
}
private logSubscribers() {
console.log(`${this.username} now has ${this.observers.length} subscribers.`);
}
}
class TwitterUserObserver implements ConcreteTwitterObserver {
constructor(public username: string) { }
public update(subject: ConcreteTwitterSubject, tweets: Tweets): void {
if (subject instanceof TwitterUserSubject) {
console.log(`${this.username} reads the tweet '${tweets.post}' of ${subject.username}`);
}
}
}
const elonMusk = new TwitterUserSubject('Elon Musk');
const bill = new TwitterUserObserver('Bill');
elonMusk.subscribe(bill);
const jeff = new TwitterUserObserver('Jeff');
elonMusk.subscribe(jeff);
elonMusk.publishNewTweet({
post: 'I am sending people to Mars',
});
elonMusk.unsubscribe(jeff);
elonMusk.publishNewTweet({
post: 'Bought Twitter!',
});
Definimos quatro interfaces: Subject
, Observer
, ConcreteTwitterSubject
e ConcreteTwitterObserver
. Criamos duas classes TwitterUserSubject e TwitterUserObserver.
Os objetos da TwitterUserSubject são autores do Twitter que podem publicar artigos. Além disso, os objetos da classe TwitterUserObserver podem realizar subscribe
e unsubscribe
para os autores usando suas funções públicas. O TwitterUserSubject
se encarrega de notificar todos os seus assinantes. Veja como fica o Output deste código:
[LOG]: "Bill subscribed to Elon Musk tweets."
[LOG]: "Elon Musk now has 1 subscribers."
[LOG]: "Jeff subscribed to Elon Musk tweets."
[LOG]: "Elon Musk now has 2 subscribers."
[LOG]: "Elon Musk published a new tweet: 'I am sending people to Mars'"
[LOG]: "Elon Musk notifies its 2 subscribers of the new tweet."
[LOG]: "Bill reads the tweet 'I am sending people to Mars' of Elon Musk"
[LOG]: "Jeff reads the tweet 'I am sending people to Mars' of Elon Musk"
[LOG]: "Jeff unsubscribed from Elon Musk tweets."
[LOG]: "Elon Musk now has 1 subscribers."
[LOG]: "Elon Musk published a new tweet: 'Bought Twitter!'"
[LOG]: "Elon Musk notifies its 1 subscribers of the new tweet."
[LOG]: "Bill reads the tweet 'Bought Twitter!' of Elon Musk"
Espero que este exemplo tenha deixado claro como funciona este pattern.
Prós e contras
Podemos citar como prós de se adotar este padrão:
Princípio Aberto/Fechado. Você pode introduzir novas classes de assinante sem precisar alterar o código do editor (e vice-versa se houver uma interface do editor).
Princípio da Responsabilidade Única (SRP) é respeitado, pois a responsabilidade de cada observador é transferida para o seu método update ao invés de ter essa lógica de negócio na Observable do objeto.
Você pode estabelecer relações entre objetos em tempo de execução.
Observers podem ser adicionados ou removidos a qualquer momento.
Para contras da utilização podemos considerar:
Se não for usado com cuidado, o padrão observer pode adicionar complexidade desnecessária.
A ordem das notificações do Observer não é confiável.
Conclusão
Espero que este exemplo tenha deixado claro como utilizar este pattern. Fico por aqui neste artigo e agradeço muito por ler! Até o próximo.