DEV Community

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

Posted on

2 1

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

Heroku

Built for developers, by developers.

Whether you're building a simple prototype or a business-critical product, Heroku's fully-managed platform gives you the simplest path to delivering apps quickly — using the tools and languages you already love!

Learn More

Top comments (0)

Sentry image

See why 4M developers consider Sentry, “not bad.”

Fixing code doesn’t have to be the worst part of your day. Learn how Sentry can help.

Learn more

👋 Kindness is contagious

Please show some love ❤️ or share a kind word in the comments if you found this useful!

Got it!