Patrón Observer (parte 2)
leobar37
Posted on October 20, 2021
En la primera parte implementé el patrón Observer, pero ligado a una lógica especifica. En este artículo quiero plantearlo de dos maneras más pero sin ligarlos a una lógica de negocio e incluyendo un poco de programación funcional.
Ejemplo 1:
Primero vamos a implementar el patrón de Observer de una manera que nos permita hacer una emisión de cualquier dato y poder suscribirnos a él.
type Unsuscribe = () => void;
type Listener<T> = (value: T) => void;
export interface ISubject<T> {
suscribe(listener: Listener<T>): Unsuscribe;
}
Este sería el planteamiento, vamos tenemos un Unsuscribe
el cual va a ser una función que me permite dejar de escuchar los cambios del sujeto y el Listener
el cual es la función que vamos a llamar para notificar los cambios y finalmente ISubject
.
export class Subject<T> implements ISubject<T> {
listeners = new Set<Listener<T>>();
next(value: T): void {
this.listeners.forEach(listener => listener(value));
}
suscribe(listener: Listener<T>): Unsuscribe {
this.listeners.add(listener);
return () => this.listeners.delete(listener);
}
}
Esta sería la implementación del patrón completa, esta clase tiene como finalidad emitir a todos los oyentes los valores que se vayan emitiendo, como un flujo de datos. Veámoslo en acción.
const obs = new Subject<string>();
obs.next('hello');
obs.suscribe(d => {
console.log('suscriber 1');
console.log(d);
});
obs.next('World');
obs.suscribe(d => {
console.log('suscriber 2');
console.log(d);
});
obs.next('bye');
Output:
Para explicarlo un poco mejor lo voy a poner gráficamente:
La palabra hello no fue mostrada porque al momento que se emitió ese dato no había ningún listener escuchando, cuando se emitió world solo había uno y cuando apareció bye habían dos.
Ejemplo 2:
Cuando tratamos con el dom a menudo vemos codigo así:
document.addEventListener('click', () => {
console.log('Hi, you click me :)');
});
Lo que indica que cuando el evento click
ocurra esa función va a ser accionada. Y eso puede ocurrir en distintos lugares(puedes escuchar el evento click
muchas veces). Ahora vamos a emplear el patrón observer de una manera diferente, ahora ya no tendremos un solo conjunto de Listeners. En ****lugar de eso vamos a tener un conjunto por cada evento.
type Listener<T> = (value: T) => void;
type Listeners<T> = Array<Listener<T>>;
interface IEventEmmiter<T> {
on(event: string, listener: Listener<T>): void;
once(event: string, listener: Listener<T>): void;
removeEventListeners(): void;
emit(event: string, payload: T): void;
}
Está seria la estructura, los métodos dentro de IEventEmmiter
tienen que cumplir con lo siguiente.
on:
Me permite suscribirme a un evento específico.
once:
Me permite suscribirme a un evento específico pero solo una vez.
removeEventListeners:
Remueve todos los oyentes,
emit:
Emite datos según el evento.
Veamos como implementamos esto 😌.
lo primero que voy hacer plantear el almacenamiento de los Oyentes, para eso voy a usar Map
.
class EventEmmiter<T> implements IEventEmmiter<T> {
private listenersManager = new Map<string, Listeners<T>>();
constructor() {}
emit(event: string, payload: T): void {}
on(event: string, listener: Listener<T>): void {}
once(event: string, listener: Listener<T>): void {}
removeEventListeners(): void {}
}
Una vez planteado esto podemos empezar a agregar código a los métodos.
Empecemos con on:
class EventEmmiter<T> implements IEventEmmiter<T> {
private listenersManager = new Map<string, Listeners<T>>();
constructor() {}
emit(event: string, payload: T): void {}
private getListeners(event: string) {
return this.listenersManager.get(event) || [];
}
on(event: string, listener: Listener<T>): void {
const listeners = this.getListeners(event);
listeners.push(listener);
this.listenersManager.set(event, listeners);
}
once(event: string, listener: Listener<T>): void {}
removeEventListeners(): void {}
}
Lo único que vamos a hacer en on
es agregar es agregar el listener
al evento que le corresponda.
Ahora veamos once
:
class EventEmmiter<T> implements IEventEmmiter<T> {
private listenersManager = new Map<string, Listeners<T>>();
private onlyOnce = new Set<Listener<T>>();
constructor() {}
emit(event: string, payload: T): void {}
private getListeners(event: string) {
return this.listenersManager.get(event) || [];
}
on(event: string, listener: Listener<T>): void {
const listeners = this.getListeners(event);
listeners.push(listener);
this.listenersManager.set(event, listeners);
}
once(event: string, listener: Listener<T>): void {
this.onlyOnce.add(listener);
this.on(event, listener);
}
removeEventListeners(): void {}
}
Para once
he creado un segundo almacenamiento donde guardaré este tipo de listeners. Quizá una forma mucho mejor de hacerlo sea envolver a al listener dentro de un objeto para no tener dos listas, pero para no complicar las cosas lo voy a dejar así.
class EventEmmiter<T> implements IEventEmmiter<T> {
private listenersManager = new Map<string, Listeners<T>>();
private onlyOnce = new Set<Listener<T>>();
constructor() {}
emit(event: string, payload: T): void {
this.getListeners(event).forEach(listener => {
if (this.onlyOnce.has(listener)) {
this.onlyOnce.delete(listener);
this.removeListener(event, listener);
}
listener(payload);
});
}
private removeListener(event: string, listener: Listener<T>) {
const newListeners = this.getListeners(event).filter(d => d !== listener);
this.listenersManager.set(event, newListeners);
}
private getListeners(event: string) {
return this.listenersManager.get(event) || [];
}
// rest of code ..
removeEventListeners(): void {}
}
Bien a la hora de emitir el evento es donde es útil la segunda lista , si el listener se encuentra dentro dentro de onlyOnce
este se tiene que eliminar.
Ahora solo faltaría el ultimo método removeListeners
.
class EventEmmiter<T> implements IEventEmmiter<T> {
private listenersManager = new Map<string, Listeners<T>>();
private onlyOnce = new Set<Listener<T>>();
constructor() {}
private getListeners(event: string) {
return this.listenersManager.get(event) || [];
}
private removeListener(event: string, listener: Listener<T>) {
const newListeners = this.getListeners(event).filter(d => d !== listener);
this.listenersManager.set(event, newListeners);
}
emit(event: string, payload: T): void {
this.getListeners(event).forEach(listener => {
if (this.onlyOnce.has(listener)) {
this.onlyOnce.delete(listener);
this.removeListener(event, listener);
}
listener(payload);
});
}
on(event: string, listener: Listener<T>): void {
const listeners = this.getListeners(event);
listeners.push(listener);
this.listenersManager.set(event, listeners);
}
once(event: string, listener: Listener<T>): void {
this.onlyOnce.add(listener);
this.on(event, listener);
}
removeEventListeners(): void {
this.onlyOnce = new Set();
this.listenersManager = new Map();
}
}
Listo una clase hermosa para manejar eventos, ahora en acción.
const emmiter = new EventEmmiter<string>();
emmiter.on('hello', name => {
console.log('sub 1');
console.log('Hello ' + name);
});
emmiter.once('hello', name => {
console.log('sub 4 (once)');
console.log('Hello ' + name);
});
let cont = 0;
setInterval(() => {
emmiter.emit('hello', 'Leobar' + cont);
if (cont == 5) {
emmiter.removeEventListeners();
}
cont++;
}, 1000);
output :
Bueno ha pasado la prueba 😌.
Bien puedo decir que fue suficiente como para entender la utilidad de este patrón por cierto los dos ejemplos estan inspirado la libreria rxjs y EventEmiter de node js respectivamente.
Posted on October 20, 2021
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.
Related
November 29, 2024