Javascript: Patrón Observador

martes, 18 de diciembre de 2012 Etiquetas: , , ,

Introducción


Unos de los patrones de diseño1 que últimamente más utilizo es es el patrón observador2, también conocido como Publish/Subscribe (Publicador / Subscriptor). Este patrón indica que un objetos (subscriptor) pueda observar a otro objeto el publicador, que informa de estados o eventos que pueden interesar a los subscriptores.


El subscriptor, mediante la llamada a un método, se subscribe para ser notificado sobre algún concepto, cuando algo interesante ocurra. Sería algo similar a cuando te suscribes para recibir noticias de algún tipo de un periódico.

La principal ventaja de este patrón es conseguir que los diferentes objetos sean independientes los unos de los otros. Los generadores de los avisos no necesitan saber si hay o no algún objeto interesado ni su número, su única función es avisar que ha ocurrido algo interesante. Los subscriptores tampoco necesitan saber el objeto que genera los avisos, sólo como tiene que subscribirse.

Implementación de Ben Alman

Una implementación muy popular en Javascript utilizando JQuery es la que realiza Ben Alman3. Vamos a verla:

(function($) {
  var o = $({});
 
  $.subscribe = function() {
   o.on.apply(o, arguments);
  };
 
  $.unsubscribe = function() {
   o.off.apply(o, arguments);
  };
 
  $.publish = function() {
   o.trigger.apply(o, arguments);
  };
}(jQuery));

Como vemos es muy sencillo y eficaz. El único inconveniente que le veo en su utilización es que cuando se llama a un objetos, this no hace referencia a dicho objeto al utilizar la función apply
.
La implementación que utilizo es la siguiente

var pubsub = {};

(function(q) {
  var topics = {};

  q.publish = function( topic, args ) {
    if ( !topics[topic] ) {
      return false;
    }
    var subscribers = topics[topic],
    len = subscribers ? subscribers.length : 0;
    while (len--) {
      subscribers[len].trigger(topic, args);
    }
    return this;
  };

  q.subscribe = function( topic, $obj, func ) {
    if (!topics[topic]) {
      topics[topic] = [];
    }
    $obj.on( topic, func);
    topics[topic].push($obj);
    return this;
  };
 
}( pubsub ));
En la implementación falta la función de quitar la suscripción, como no la he utilizado la he quitado. Es bastante sencilla de comprender y de utilizar. Al pasar el objeto y realizar la llamada del evento desde el mismo, al utilizar this en la función que recibe el aviso, estamos accediendo al objeto que nos interesa, por lo menos a mi me es mas cómodo así.

¿Cómo lo utilizo?

Como ya he comentado, la principal ventaja es desacoplar las dependencia de los objetos. Veamos un ejemplo, supongamos que tenemos una clase que realiza una llamada Json para obtener los datos de ventas, con los datos queremos mostrar un gráfico de la evolución y una tabla para mostrarlos. Cómo la llamada necesita muchos parámetros hemos realizad una clase. Así podríamos realizar una primera aproximación:

function SearchInvoices(table, graph){
  this.dir='http://www.acme.com';
  this.dateIni = '01/01/2012';
  this.dateEnd = '31/12/2012';
  this.costumer = '';
  this.table = table;
  this.graph = graph;
}

SearchInvoices.prototype={
/*
  ....
  Funciones que hacen que cuando se cambie un valor en el formulario de 
  fecha o cliente se realice automáticamente la consulta llamando a search
  ....
*/

search: function( ){  
  var dirJson = dir+'/json';
  $.getJSON(dirJson, { dateini : this.dateIni, 
                    dateend : this.dateEnd, 
                    costumer : this.costumer
                     },
            function (data){
              table.show(data); 
              graph.draw(data);
            }); 
  }
}
Lo probamos y funciona perfectamente. Al cabo del tiempo tenemos que realizar una pantalla similar, pero esta vez sólo quieren que aparezca la tabla de datos, con lo que el objeto de la llamada tal y como esta no nos vale. Tenemos que modificarlo, podemos añadir un parámetro para indicar si se quiere mostrar la tabla y el gráfico o por separado. Pero si dentro de un tiempo surge otro consumidor de la información tendríamos que volver a realizar el apaño.

Nuestra función de obtención de datos es buena realizando la consulta de datos, así que dejemos que realice solo esa tarea, y que avise a los posibles consumidores de los datos. Vamos a ver como quedaría con el patrón observador.

function SearchInvoices(){
  this.dir='http://www.acme.com';
  this.dateIni = '01/01/2012';
  this.dateEnd = '31/12/2012';
  this.costumer = '';
}
 
SearchInvoices.prototype={
/*
  ....
  Funciones que hacen que cuando se cambie un valor en el formulario de 
  fecha o cliente se realice automáticamente la consulta llamando a search
  ....
*/
 
search: function( ){  
  var dirJson = dir+'/json';
  $.getJSON(dirJson, { dateini : this.dateIni, 
                    dateend : this.dateEnd, 
                    costumer : this.costumer
                     },
            function (data){
              pubsub.publish( 'invoice.data', [ data ] );
            }); 
  } 
}

function TableInvoice(){
  pubsub.subscribe('invoice.data', $(this), this.show);
}

TableInvoice.prototype = {
  /*...*
  show: function(event, data){
    /* Realiza operaciones*/
  }
  /*...*
}

function Graph(){
}

TableInvoice.prototype = {
  /*...*
  draw: function(event, data){
    /* Realiza operaciones*/
  }
  /*...*
}

$(document).ready(function() {
  var graph = new Graph();
  pubsub.subscribe('invoice.data', $(graph), graph.draw);
});
 
Como vemos ahora SearchInvoices solo se preocupa de leer los datos y avisar que los ha recibido, no se preocupa quien los tiene que consumir, simplemente se preocupa de hacerlos públicos, los interesados se habrán subscrito para recibirlos. De esta forma podemos la dos partes son independientes unas de otras y las podemos reutilizar mas fácilmente.

En el ejemplo hay dos formas de subscribirse, una lo realiza en el constructor y la otra la deja a elección del programador subscribirse o no.

Bibliografia

[1] Wikipedia Patrones de Diseño
[2] Wikipedia Patrón Observador
[3] GitHub. Ben Alman

0 comentarios:

Publicar un comentario