BLOG

We write about great things, you know.

Reinventing brands with high value ideas

Notification Center Pattern

Por: Diego Marafetti

ago 1, 2013 • codingNo hay comentarios

Uno de los patrones de diseño más útiles que hemos utilizado en el desarrollo de nuestras aplicaciones es el Notification Center. Este patrón es una variante del Observer Pattern y está basado en el servicio de notificaciones de Mozilla con algunas modificaciones propias de nuestra implementación en Javascript.

Problemática

A medida que la UI crece en complejidad, un evento puede generar cambios en todos los componentes que la forman. Programáticamente hablando, si un componente recibe un evento (ya sea del usuario o no) y otro componente debe cambiar su estado en relación a este evento entonces el primero deberá enviarle un mensaje al segundo notificando sobre dicho evento. Esto implicaría que el receptor del evento mantenga una referencia fuerte al segundo componente.

Si multiplicamos esta relación varias veces nos encontramos con muchos componentes referenciados entre sí. Estoy hablando de una interface de usuario muy compleja. Para cuando nos demos cuenta, nuestro Javascript será inmanejable, dando lugar a muchísimos bugs y el debugging se tornará muy complicado.

Imaginemos el caso de una UI donde existe un árbol de directorios, donde cada uno tiene una lista de imágenes que pueden ser cargadas en una vista previa y una barra de opciones con acciones aplicables a dichas imágenes (cortar, rotar, borrar, etc). La distribución quedaría de esta manera:

nc-wireframe

Ahora pensemos en algunos casos de uso sobre esta pantalla:

El primer evento que el usuario genera es un “click” sobre un determinado directorio. Esto hará que las imágenes de dicho directorio se listen bajo esa rama del árbol. El usuario puede a su vez seleccionar una imagen, lo que generará que la preview aparezca en pantalla.


// filename: Tree.js

onNodeClick: function(dirNode) {

dirNode.listFiles();
}

onFileNodeClick: function(fileNode) {

    var filename = fileNode.getFilename();
    this.previewWidget.openFile(filename);
    this.directoryWidget.show();
}

Otro caso de uso es cuando el usuario decide rotar imágenes “clickeando” el botón de “rotate” en la barra de opciones. En consecuencia la imagen deberá actualizarse; ¿y si el usuario decide eliminar la imagen? Entonces la preview debe desaparecer y el listado debe actualizarse al igual que el árbol.


// Filename: Optionbar.js

onRotateClick: function() {

    this.previewWidget.rotate();
}

onRemoveImageClick: function() {

    var filename = this.previewWidget.getCurrentFile();
    this.previewWidget.closeFile();
    this.directoryWidget.removeFile(filename);
}

Si realizamos un grafo que muestre las relaciones entre cada componente, siendo cada arista la referencia, observamos que todos los componentes tiene una referencia a los otros dos.

nc-grafo

El objetivo de utilizar el Notification Center es el de reducir la cantidad de referencias a otros objetos.

Unificamos el sistema de mensajes en un sólo objeto que se encargue de recibirlos y enviarlos. Pensémoslo como canal de mensajería, transversal a todo el sistema, donde cada uno de estos representa un suceso en el sistema y en el que los objetos que estén escuchando puedan cambiar su estado según el evento que les interese.

Estos mensajes o eventos dejan de tener un receptor único y pasar a ser mensajes enviados a todos los receptores que estén interesados en recibirlo. Sería un broadcast a un grupo de objetos cuya implementación el notificador desconoce.

Con esto desacoplaríamos componentes visuales que no necesitan conocerse (personalmente piense que la excepción sería una composición de objetos).

[User Interface Component Model]: En este artículo, cuando hablo de componente, hablo de elementos configurables y reusables que componen la interface de usuario. Puede ser simple como un campo de texto o un botón o puede ser algo más complejo como un árbol de directorio, un datepicker o una preview de imagen.

Nuestro objetivo

* Separar la interacción entre el UI y nuestra lógica de negocio.

* Desacoplar componentes de UI.

* Minimizar la complejidad de código.

¿Cómo implementaríamos este servicio?

La solución que desarrollamos fue la de mantener una tabla de topics y una colección de objetos suscritos a dicho topic. En el proceso de notificación, se accederá directamente a los suscriptores usando como indice el nombre del topic. Luego se enviará el mensaje correspondiente.

Nuestro Notification Center difiere un poco con la implementación de Mozilla. En nuestro caso queremos que todos los suscriptores que reciban la notificación sean sólo los que se hayan suscrito a ese topic, pero además que notifique directamente a una función callback asociada al topic. De otra manera, tendríamos que agregar una condición en la función callback preguntando si es el topic que nos interesa, ya que un observer puede escuchar varios topics a la vez. Para evitar esto utilizamos una convención donde el nombre de la función callback que el Subject llama es el nombre del topic. Veamos un ejemplo:

topic ==> "itemSelected" ==> callback ==> "onItemSelected"


La convención dice que todo observer que se suscriba a un determinado Topic deberá declarar en su interface una función con el nombre de “on” + “topic” en camel-case.

Notification Center Class

En nuestro caso, cada componente de UI que se suscriba sería un Observer de un determinado Topic en el sistema. La interface de nuestro notificador, en su forma más básica, implementaría las siguientes funciones:

/**
 * @param anObserver es cualquier objeto que implemente
 *        la interface topic/function.
 * @param aTopic es un string que identifica el topic. Es único en el sistema.
**/
addObserver: function(anObserver, aTopic);


A su vez, cualquier componente también puede funcionar como Subject de la relación. Ante un evento, el componente puede notificar a todos los observadores de ese evento (o topic) que el mismo ha sucedido.

/**
 * @param aTopic es el evento que debe ser notificado.
 * @param someDataEs. posible enviar datos a todos los observers a través
 *                    del parametro someData. Sería como una especie de DTO.
 *
**/
notify: function(aTopic, someData);


 Por último, cualquier componente puede desuscribirse del sistema de notificaciones utilizando la siguiente función.

/**
 * @param anObserver la referencia al objeto que quiere ser removido
 * @param aTopic es el topic del cual va ser removido
**/
removeObserver: function(anObserver, aTopic);


Veamos un ejemplo de cómo utilizar este servicio. En este caso el Image Preview se suscribe al evento “imageSelected” el cual le notificará que una imagen fue seleccionada. Fijense que no existe referencia alguna al objeto que seleccionó la imagen:


// Filename: ImagePreview.js
/**
 * Class constructor or init code
 **/
init: function() {

     NotificationCenter.addObserver(this, ‘imageSelected’);
}
/**
 * The user has selected an item from the tree
**/
onImageSelected: function(imageUri) {

     this.loadImage(imageUri);
}


El objeto que representa el árbol de directorios notifica que un archivo fue seleccionado de la siguiente forma. No le importa a quién va dirigido el mensaje ya que no es su responsabilidad hacer algo al respecto:


// Filename: Tree.js

/**
 * The user has clicked a tree's node
 **/
onFileNodeClick: function(fileNode) {

    NotificationCenter.notify(‘imageSelected’, fileNode.getFileUri());
}


Este patrón fue utilizado con éxito en varias aplicaciones web de gran tamaño. También fue portado a Actionscript 3 para un desarrollo en Playbook y se utilizó en una aplicación Java Swing. Otros frameworks como Cocoa para el caso de aplicaciones iOS ya tienen internamente algunas variantes como es el caso del NSNotificationCenter (este es el que más se acerca a nuestra implementación).

Tags: , , , , , , ,

Deja un comentario

Tu dirección de correo electrónico no será publicada. Los campos necesarios están marcados *


nueve − 1 =

Puedes usar las siguientes etiquetas y atributos HTML: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>