Observer Pattern
- Este é um pattern comportamental
- Ele é chamadado por outros nomes também, como: Pub/Sub, Event-Subscribe, Listener
- É um mecanismo de notificação para vários objetos sobre qualquer evento em um objeto observado
- Este padrão é descrito em duas partes: Quem observa os eventos e quem é observável.
O que é o Observer
- É alguém que observa algo e reage baseado nas ações de quem ele observa
- Ele define que o eventos que reagir de quem observa
class Observer {
public handler: (data: any) => Promise<void>;
constructor(
public readonly eventName: string,
handler: (data: any) => Promise<void>
) {
this.handler = handler;
}
}
O que é um Observable
- Algo que é observável por alguém externo a ele, que é o observador
- Possue diversos observadores
- Ele notifica os observadores
- Ele registrar quem quer observá-lo
class Observable {
private _observers: Observer[];
// registra novos observadores
addObserver(observer: Observer) {
this._observers.push(observer);
}
// notifica todos os observadores que esperam um determinado evento
notify(eventName: string, data: any) {
this._observers
.filter((observer) => observer.eventName === eventName)
.forEach((observer) => observer.handler(data));
}
}
Exemplo 1
- Eu tenho uma entidade Usuário que podemos observar mudanças no seu estado e/ou ações realizadas
- Então ela herdas os comportamentos necessários que a torna observável (
Observable
) - Mas antes eu defino quais evento esta entidade realiza
const EVENTS = {
INSTANCE_USER: 'instance-user',
UPDATE_USER_NAME: 'update-user-name',
UPDATE_USER_EMAIL: 'update-user-email',
}
class User extends Observable {
public name: string;
public email: string;
constructor(name: string, email: string, instanceEvent?: Observer) {
super();
this.name = name;
this.email = email;
if (instanceEvent) {
this.addObserver(instanceEvent);
}
this.notify(EVENTS.INSTANCE_USER, this);
}
update(name?: string, email?: string) {
const previous = { ...this }
this.name = name || this.name;
this.email = email || this.email;
if (email)
this.notify(EVENTS.UPDATE_USER_EMAIL, { previous: previous.email, current: email });
if (name)
this.notify(EVENTS.UPDATE_USER_NAME, { previous: previous.name, current: name });
}
}
- O próximo passo é criar os observadores desses eventos e registrá-los na em quem quero observar os evento, no caso nossa entidade User
const userInstancedEvent = new Observer(EVENTS.INSTANCE_USER, async (user: User) => {
console.log(`User ${user.name} created!`);
});
const userEmailUpdatedEvent = new Observer(EVENTS.UPDATE_USER_EMAIL, async (data:{ previous: string, current: string }) => {
console.log(`User ${data.previous} updated to ${data.current}!`);
});
const userNameUpdatedEvent = new Observer(EVENTS.UPDATE_USER_NAME, async (data: { previous: string, current: string }) => {
console.log(`User ${data.previous} updated to ${data.current}!`);
});
-
userInstancedEvent
: Observar quando um usuário for instanceado e mostar no console o nome dele -
userEmailUpdatedEvent
: Observar quando um usuário alterar seu email e mostar no console -
userNameUpdatedEvent
: Observar quando um usuário alterar seu nome e mostar no console -
Já temos o cenário para para criar um usuário e reagir aos eventos dele
const user = new User('John Doe', '[email protected]', userInstancedEvent);
user.addObserver(userNameUpdatedEvent);
user.addObserver(userEmailUpdatedEvent);
user.update('Melk de Sousa')
user.update(undefined, '[email protected]')
user.update(undefined, '[email protected]')
- O resultado em nosso console é...
User John Doe created!
User John Doe updated to Melk de Sousa!
User [email protected] updated to [email protected]!
User [email protected] updated to [email protected]!
Exemplo 2
- Todo software, praticamente, tem a funcionalidade de realizar um Login.
- Mas imagine que é preciso tomar alguma decisão quando um login falha ou tem sucesso
- O Observer Pattern pode ser muito bem aplicável
type Input = {
username: string;
password: string;
};
export class LoginUseCase extends Observable {
async execute({ username, password }: Input) {
try {
// do something
this.notify("success", undefined);
} catch (error) {
this.notify("fail", undefined);
}
}
}
- Agora é possível criar os observers de quando o login falhar e tiver sucesso
export const loginSuccess = new Observer('success', async () => {
console.log('Login success');
});
export const loginFail = new Observer('fail', async () => {
console.log('Login fail');
})
- Com os observers e o observable definidos, só é preciso fazer o registro deles no Login
const loginUseCase = new LoginUseCase();
loginUseCase.addObserver(loginFail)
loginUseCase.addObserver(loginSuccess)
loginUseCase.execute({ username: 'admin', password: 'admin' });
- A partir desse momento, toda vez que um login tiver sucesso, será mostrado uma mensagem no console. O mesmo se aplica em caso de falha.
- Mas dependendo do cenário, é possível aplicar trativas melhores que isso, como enviar para um serviço externo de logs, filas, etc.
Conclusão
- É incrível as possibilidades que esse pattern no proporciona
- Não é atoa sua presença na maioria dos sistemas e framworks
- Vale a pena ver o cenários que pode ser aplicado em suas aplicações, bem como o seu tradeoff