DEV Community

Cover image for Patrón Observer (parte 2)
leobar37
leobar37

Posted on

Patrón Observer (parte 2)

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;
}
Enter fullscreen mode Exit fullscreen mode

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);
  }
}
Enter fullscreen mode Exit fullscreen mode

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');

Enter fullscreen mode Exit fullscreen mode

Output:

Image description

Para explicarlo un poco mejor lo voy a poner gráficamente:

Image description

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 :)');
});
Enter fullscreen mode Exit fullscreen mode

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;
}

Enter fullscreen mode Exit fullscreen mode

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 {}
}
Enter fullscreen mode Exit fullscreen mode

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 {}
}
Enter fullscreen mode Exit fullscreen mode

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 {}
}
Enter fullscreen mode Exit fullscreen mode

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 {}
}
Enter fullscreen mode Exit fullscreen mode

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();
  }
}
Enter fullscreen mode Exit fullscreen mode

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);
Enter fullscreen mode Exit fullscreen mode

output :

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.

codigo completo

Top comments (0)