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.
Top comments (0)