Patrón Observer

Para hablar del patrón Observer, antes deberíamos "empezar por el principio" (me encanta cuando la gente utiliza esa expresión redudante). En la teoría del software, los patrones de diseño nos ayudan a resolver los problemas más comunes a los que nos enfrentamos con una serie de soluciones a éstos. Podríamos hacer un poco de historia, pero no es el objetivo de este artículo contaros batallitas 😛. De forma resumida, hace muchos años ya se conocían bastantes patrones pero dicho conocimiento estaba disperso, de forma que la conocida como "Banda de los Cuatro" (Erich Gamma, Richard Helm, Ralph Johnson y John Vlissides) decidieron escribir un libro que recogiera la definición de cada uno de los 23 patrones que se conocían en su momento. Dicho libro se llama Patrones de Diseño, por lo que es un buen punto de partida para quienes queráis ahondar más en este tema.

Estos patrones se clasificaron en tres categorías:

  • Patrones creacionales: como su nombre indica, permiten ayudar en la instanciación de clases.
  • Patrones estructurales: estos patrones, permiten definir las relaciones entre clases y su composición.
  • Patrones de comportamiento: definen como pueden interaccionar y comportarse las clases.

En el artículo de hoy, aprenderemos en qué consiste el Patrón Observer, atendiendo primero a su definición estricta, y luego viendo un ejemplo del mundo real. Como siempre, cualquier duda, comentario o corrección que queráis hacer la podéis dejar en comentarios.

Teoría del patrón Observer

Si atendemos a la definición estricta. Este patrón:

Define una dependencia uno a muchos entre objetos. De forma que cuando un objeto (subject) cambie de estado, este cambio se notificará a aquellos otros objetos (observers) que estén "suscritos" a él.

classDiagram class Subject { -Observer[] observers +addObserver(Observer) void +removeObserver(Observer) void +notify() void } class ConcreteSubject { -SubjectStatus status +setStatus(SubjectStatus) void +getStatus() SubjectStatus } class Observer { interface +update() } class ConcreteObserver { -ObserverStatus status } ConcreteSubject "1" o-- "0..*" ConcreteObserver Subject "1" o-- "0..*" Observer ConcreteSubject --> Subject ConcreteObserver --> Observer

En el diagrama superior podemos distinguir las siguientes clases:

  • Subject: esta clase gestiona la suscripción de los observadores. Guardará la lista de objetos suscritos para poder notificarles cuando hay cambios.
  • Observer: tendrá un método público al que los sujetos podrán llamar cuando le quieran notificar.
  • ConcreteSubject: almacena un estado concreto para una serie de observadores.
  • ConcreteObserver: se suscribe a un sujeto concreto y se encarga de actualizar su estado interno a partir del estado del sujeto, cuando éste último le notifique.

La secuencia que seguiría el flujo original del patrón sería la siguiente:

sequenceDiagram participant S as ConcreteSubject participant O1 as ConcreteObserver1 participant O2 as ConcreteObserver2 Note over O2,S: Al instanciar los observadores O1->>S: addObserver(this) O2->>S: addObserver(this) Note over O2,S: Ejemplo de interacción O1->>S: setStatus(newStatus) activate S S->>S: notify() activate S S->>O1: update() activate O1 O1->>S: getStatus() activate S S-->>O1: deactivate O1 deactivate S S->>O2: update() activate O2 O2->>S: getStatus() S-->>O2: deactivate O2 deactivate S deactivate S Note over S,O2: Antes de destruir los observadores O1->>S: removeObserver() O2->>S: removeObserver()

Tras tanto diagrama os explico un poco. Prácticamente lo que hacemos es que el observador se suscriba al sujeto. En el momento en que el sujeto cambia su estado (en la definición del patrón, este cambio lo hace el propio observador), éste notifica a todos los observadores para que puedan consultar el nuevo estado del sujeto.

Un ejemplo práctico

Voy a simplificar mucho el código en TypeScript para que veáis un ejemplo muy tontorrón quitando algunas clases, pero que creo que os puede ayudar a entenderlo mejor:

interface IObserver {
  update(): void;
}

class Sujeto {
  private name: string;
  private observers: IObserver[] = [];

  public addObserver(observer: IObserver): void {
    this.observers.push(observer);
  }

  public getName(): string {
    return this.name;
  }

  // Cuando cambia el estado, pasamos a notificar a todos los observadores.
  public setName(newName: string): void {
    this.name = newName;
    this.notify();
  }

  private notify(): void {
    for (const observer of this.observers) {
      observer.update();
    }
  }
}

class Observador implements IObserver {
  private id: string;
  private name: string;
  private sujeto: Sujeto;

  public constructor(id: string, sujeto: Sujeto) {
    this.id = id;
    this.sujeto = sujeto;
    this.name = this.sujeto.getName();

    // Suscripción al sujeto para poder recibir sus notificaciones cuando cambie su estado.
    sujeto.addObserver(this);
  }

  // Implementación de la interface IObserver para que el sujeto nos pueda notificar.
  public update(): void {
    console.log("Notificado " + this.id);
    this.name = this.sujeto.getName();
    console.log("En " + this.id +  " ahora el nombre es " + this.name);
  }
}

Pues bien, una vez que hemos definido las dos clases simplonas, veamos que pasaría si ejecutamos algo de código para probarlo:

const sujeto: Sujeto = new Sujeto();
const observador1: Observador = new Observador("observador1", sujeto);
const observador2: Observador = new Observador("observador2", sujeto);

sujeto.setName("Pepito");

// Por consola veremos lo siguiente:
// 'Notificado observador1'
// 'Notificado observador2'
// 'En observador1 ahora el nombre es Pepito'
// 'En observador2 ahora el nombre es Pepito'

Se puede ver como en cuanto realizamos un cambio de estado en el sujeto, este notifica a todos los observadores que se hayan suscrito. Así que, aunque hemos visto el patrón de forma muy simplificada, creo que os puede dar una pista de como es su funcionamiento.

Un ejemplo del mundo real

Si eres una persona nueva en el mundo de la programación, más exactamente si acabas de empezar con JavaScript, quizás no sepas que has estado usando el patrón observer de forma continua. Y lo has usado cuando has llamado al método addEventListener().

Para gente que lo desconozca, hacemos un breve inciso. En JavaScript podemos escuchar eventos que se lancen sobre un componente HTML, por ejemplo, podemos escuchar el evento "click" sobre un botón, de forma que cada vez que se haga clic en dicho botón, seamos notificados. Para ello el esquema básico sería:

elementoHTML.addEventListener("tipoDeEvento", funcionALaQueNotificar);

Y seguimos tras esta breve aclaración 😉.

El sistema que se utiliza en JavaScript para ser notificados cuando ocurre un evento, podríamos decir que es una patrón Observer "mutado", ya que tiene cuatro diferencias principalmente:

  • Por un lado, la suscripción se realiza sobre un "cambio" concreto del sujeto, en lugar de realizar una suscripción general. En este caso te suscribes al evento para el que quieres ser notificado.
  • Muchos de los patrones de diseño se pueden aplicar a funciones, por lo que no estamos limitados a utilizar siempre clases para utilizarlos. Precisamente este es un ejemplo del funcionamiento del patrón Observer con funciones, ya que al suscribirse se indicará la función que será llamada.
  • La notificación también cambia, ya que en lugar de sólo avisar que hay un cambio por parte del sujeto, éste lo que hará es enviar directamente los datos involucrados en el cambio, con lo que la función que hemos usado como observadora, recibirá, en este caso, los datos del evento lanzado.
  • Finalmente, el sujeto no notifica cambios en su estado. Es decir, que en el evento que recibimos, se incluyen los datos básicos del sujeto. Pero los datos del evento en sí, no son parte del estado del sujeto, sino una acción llevada a cabo sobre este.

Un diagrama muy básico de como funciona sería el siguiente:

sequenceDiagram participant N as Navegador participant H as ElementoHTML participant F1 as FuncionObservadora1 participant F2 as FuncionObservadora2 N->>H: evento(eventType) activate H H->>H: processEvent(eventType) activate H H->>F1: call(eventData) H->>F2: call(eventData) deactivate H deactivate H

Cosillas a tener en cuenta

Hemos visto la definición estándar del patrón, así como una implementación del mundo real con algunos cambios. Precisamente, una de las cosas a tener en cuenta es que los patrones de diseño no son algo cerrado que debas seguir al 100%, ya que dependiendo del caso, puede que te interese hacer algunos ajustes. En el caso del patrón observer, hemos visto como se puede expandir con: suscripciones a datos en particular, notificaciones con datos en la llamada, suscripción de funciones en lugar de clases y que los observadores (las funciones en este caso), no están lanzando el cambio que provoca las notificaciones. Como conclusión podemos decir que los patrones son los planos con los que trabajar, pero no impide que podamos hacer correcciones a éstos para ajustarlos a nuestras necesidades.

Otro de los puntos importantes en este patrón, es que los observadores no saben el coste de actualizar el sujeto, ya que no conocer al resto de observadores. Me explico: si cada observador trata la notificación de forma síncrona, cualquier observador que bloquee el hilo principal con algún proceso que lleve demasiado tiempo, dejará al resto en espera. De ahí que es ideal evitar el tener observadores bloqueantes sin vamos a trabajar en modo síncrono.

Y bien, hasta aquí los datos básicos del patrón Observer, espero que os pueda ayudar cuando surja la necesidad de utilizarlo en vuestro proyectos. Y sin más, me despido hasta el próximo artículo.

Comparte este artículo con quien quieras
Optimiza tus transacciones con ACID
Breve: Sección de bibliografía

Discussion

Leave a Comment

Your email address will not be published. Required fields are marked *

Este sitio usa Akismet para reducir el spam. Aprende cómo se procesan los datos de tus comentarios.