¿Por qué se usaría el patrón Publicar / Suscribirse (en JS / jQuery)?


103

Entonces, un colega me presentó el patrón de publicación / suscripción (en JS / jQuery), pero me está costando entender por qué uno usaría este patrón sobre JavaScript / jQuery 'normal'.

Por ejemplo, anteriormente tenía el siguiente código ...

$container.on('click', '.remove_order', function(event) {
    event.preventDefault();
    var orders = $(this).parents('form:first').find('div.order');
    if (orders.length > 2) {
        orders.last().remove();
    }
});

Y pude ver el mérito de hacer esto en su lugar, por ejemplo ...

removeOrder = function(orders) {
    if (orders.length > 2) {
        orders.last().remove();
    }
}

$container.on('click', '.remove_order', function(event) {
    event.preventDefault();
    removeOrder($(this).parents('form:first').find('div.order'));
});

Porque introduce la capacidad de reutilizar la removeOrderfuncionalidad para diferentes eventos, etc.

Pero, ¿por qué decidiría implementar el patrón de publicación / suscripción e ir a las siguientes longitudes, si hace lo mismo? (Para su información, usé jQuery tiny pub / sub )

removeOrder = function(e, orders) {
    if (orders.length > 2) {
        orders.last().remove();
    }
}

$.subscribe('iquery/action/remove-order', removeOrder);

$container.on('click', '.remove_order', function(event) {
    event.preventDefault();
    $.publish('iquery/action/remove-order', $(this).parents('form:first').find('div.order'));
});

He leído sobre el patrón con seguridad, pero no puedo imaginar por qué esto sería necesario. Los tutoriales que he visto que explican cómo implementar este patrón solo cubren ejemplos tan básicos como el mío.

Imagino que la utilidad del pub / sub se haría evidente en una aplicación más compleja, pero no puedo imaginarme una. Me temo que me estoy equivocando por completo; ¡pero me gustaría saber el punto si hay uno!

¿Podría explicar brevemente por qué y en qué situaciones este patrón es ventajoso? ¿Vale la pena usar el patrón pub / sub para fragmentos de código como mis ejemplos anteriores?

Respuestas:


222

Se trata de un acoplamiento flexible y una responsabilidad única, que va de la mano con los patrones MV * (MVC / MVP / MVVM) en JavaScript, que son muy modernos en los últimos años.

El acoplamiento suelto es un principio orientado a objetos en el que cada componente del sistema conoce su responsabilidad y no se preocupa por los otros componentes (o al menos trata de no preocuparse por ellos tanto como sea posible). El acoplamiento flojo es bueno porque puede reutilizar fácilmente los diferentes módulos. No está acoplado con las interfaces de otros módulos. Al usar publicar / suscribirse, solo se combina con la interfaz de publicación / suscripción, que no es un gran problema, solo dos métodos. Entonces, si decide reutilizar un módulo en un proyecto diferente, simplemente puede copiarlo y pegarlo y probablemente funcionará o al menos no necesitará mucho esfuerzo para hacerlo funcionar.

Cuando hablamos de acoplamiento flojo debemos mencionar la separación de preocupaciones. Si está creando una aplicación utilizando un patrón arquitectónico MV *, siempre tiene un Modelo (s) y una Vista (s). El modelo es la parte comercial de la aplicación. Puedes reutilizarlo en diferentes aplicaciones, por lo que no es buena idea acoplarlo con la Vista de una sola aplicación, donde quieras mostrarlo, porque generalmente en las diferentes aplicaciones tienes diferentes vistas. Por lo tanto, es una buena idea usar publicar / suscribirse para la comunicación Modelo-Vista. Cuando su modelo cambia, publica un evento, la vista lo detecta y se actualiza. No tiene ninguna sobrecarga de la publicación / suscripción, lo ayuda para el desacoplamiento. De la misma manera, puede mantener la lógica de su aplicación en el controlador, por ejemplo (MVVM, MVP no es exactamente un controlador) y mantener la vista lo más simple posible. Cuando su Vista cambia (o el usuario hace clic en algo, por ejemplo), simplemente publica un nuevo evento, el Controlador lo detecta y decide qué hacer. Si está familiarizado con elPatrón MVC o con MVVM en tecnologías de Microsoft (WPF / Silverlight) puede pensar en la publicación / suscripción como el patrón Observer . Este enfoque se utiliza en marcos como Backbone.js, Knockout.js (MVVM).

Aquí hay un ejemplo:

//Model
function Book(name, isbn) {
    this.name = name;
    this.isbn = isbn;
}

function BookCollection(books) {
    this.books = books;
}

BookCollection.prototype.addBook = function (book) {
    this.books.push(book);
    $.publish('book-added', book);
    return book;
}

BookCollection.prototype.removeBook = function (book) {
   var removed;
   if (typeof book === 'number') {
       removed = this.books.splice(book, 1);
   }
   for (var i = 0; i < this.books.length; i += 1) {
      if (this.books[i] === book) {
          removed = this.books.splice(i, 1);
      }
   }
   $.publish('book-removed', removed);
   return removed;
}

//View
var BookListView = (function () {

   function removeBook(book) {
      $('#' + book.isbn).remove();
   }

   function addBook(book) {
      $('#bookList').append('<div id="' + book.isbn + '">' + book.name + '</div>');
   }

   return {
      init: function () {
         $.subscribe('book-removed', removeBook);
         $.subscribe('book-aded', addBook);
      }
   }
}());

Otro ejemplo. Si no le gusta el enfoque MV *, puede usar algo un poco diferente (hay una intersección entre el que describiré a continuación y el último mencionado). Simplemente estructura tu aplicación en diferentes módulos. Por ejemplo, mire Twitter.

Módulos de Twitter

Si miras la interfaz, simplemente tienes diferentes cajas. Puede pensar en cada caja como un módulo diferente. Por ejemplo, puede publicar un tweet. Esta acción requiere la actualización de algunos módulos. En primer lugar, tiene que actualizar los datos de su perfil (cuadro superior izquierdo) pero también tiene que actualizar su línea de tiempo. Por supuesto, puede mantener referencias a ambos módulos y actualizarlos por separado usando su interfaz pública, pero es más fácil (y mejor) publicar un evento. Esto facilitará la modificación de su aplicación debido al acoplamiento más flojo. Si desarrolla un nuevo módulo que depende de nuevos tweets, simplemente puede suscribirse al evento "publicar-tweet" y manejarlo. Este enfoque es muy útil y puede hacer que su aplicación sea muy desacoplada. Puede reutilizar sus módulos muy fácilmente.

Aquí hay un ejemplo básico del último enfoque (este no es el código original de Twitter, es solo una muestra mía):

var Twitter.Timeline = (function () {
   var tweets = [];
   function publishTweet(tweet) {
      tweets.push(tweet);
      //publishing the tweet
   };
   return {
      init: function () {
         $.subscribe('tweet-posted', function (data) {
             publishTweet(data);
         });
      }
   };
}());


var Twitter.TweetPoster = (function () {
   return {
       init: function () {
           $('#postTweet').bind('click', function () {
               var tweet = $('#tweetInput').val();
               $.publish('tweet-posted', tweet);
           });
       }
   };
}());

Para este enfoque hay una excelente charla de Nicholas Zakas . Para el enfoque de MV *, los mejores artículos y libros que conozco son publicados por Addy Osmani .

Inconvenientes: debe tener cuidado con el uso excesivo de publicar / suscribirse. Si tiene cientos de eventos, puede resultar muy confuso administrarlos todos. También puede tener colisiones si no usa el espacio de nombres (o no lo usa de la manera correcta). Una implementación avanzada de Mediator que se parece mucho a una publicación / suscripción se puede encontrar aquí https://github.com/ajacksified/Mediator.js . Tiene espacio de nombres y características como "burbujeo" de eventos que, por supuesto, se puede interrumpir. Otro inconveniente de publicar / suscribirse es la prueba unitaria dura, puede resultar difícil aislar las diferentes funciones en los módulos y probarlas de forma independiente.


3
Gracias, eso tiene sentido. Estoy familiarizado con el patrón MVC ya que lo uso todo el tiempo con PHP, pero no lo había pensado en términos de programación impulsada por eventos. :)
Maccath

2
Gracias por esta descripción. Realmente me ayudó a comprender el concepto.
flybear

1
Esa es una excelente respuesta. No pude evitar votar esto :)
Naveed Butt

1
Gran explicación, múltiples ejemplos, más sugerencias de lectura. A ++.
Carson

16

El objetivo principal es reducir el acoplamiento entre el código. Es una forma de pensar un tanto basada en eventos, pero los "eventos" no están vinculados a un objeto específico.

Escribiré un gran ejemplo a continuación en un pseudocódigo que se parece un poco a JavaScript.

Digamos que tenemos una radio de clase y un relé de clase:

class Relay {
    function RelaySignal(signal) {
        //do something we don't care about right now
    }
}

class Radio {
    function ReceiveSignal(signal) {
        //how do I send this signal to other relays?
    }
}

Siempre que la radio reciba una señal, queremos que varios relés transmitan el mensaje de alguna manera. El número y los tipos de relés pueden diferir. Podríamos hacerlo así:

class Radio {
    var relayList = [];

    function AddRelay(relay) {
        relayList.add(relay);
    }

    function ReceiveSignal(signal) {
        for(relay in relayList) {
            relay.Relay(signal);
        }
    }

}

Esto funciona bien. Pero ahora imagina que queremos un componente diferente para que también forme parte de las señales que recibe la clase Radio, a saber, los Altavoces:

(lo siento si las analogías no son de primera categoría ...)

class Speakers {
    function PlaySignal(signal) {
        //do something with the signal to create sounds
    }
}

Podríamos repetir el patrón nuevamente:

class Radio {
    var relayList = [];
    var speakerList = [];

    function AddRelay(relay) {
        relayList.add(relay);
    }

    function AddSpeaker(speaker) {
        speakerList.add(speaker)
    }

    function ReceiveSignal(signal) {

        for(relay in relayList) {
            relay.Relay(signal);
        }

        for(speaker in speakerList) {
            speaker.PlaySignal(signal);
        }

    }

}

Podríamos hacer esto aún mejor creando una interfaz, como "SignalListener", de modo que solo necesitemos una lista en la clase Radio, y siempre podamos llamar a la misma función en cualquier objeto que tengamos que quiera escuchar la señal. Pero eso aún crea un acoplamiento entre cualquier interfaz / clase base / etc. que decidamos y la clase Radio. Básicamente, siempre que cambie una de las clases de Radio, Señal o Retransmisión, debe pensar en cómo podría afectar a las otras dos clases.

Ahora intentemos algo diferente. Creemos una cuarta clase llamada RadioMast:

class RadioMast {

    var receivers = [];

    //this is the "subscribe"
    function RegisterReceivers(signaltype, receiverMethod) {
        //if no list for this type of signal exits, create it
        if(receivers[signaltype] == null) {
            receivers[signaltype] = [];
        }
        //add a subscriber to this signal type
        receivers[signaltype].add(receiverMethod);
    }

    //this is the "publish"
    function Broadcast(signaltype, signal) {
        //loop through all receivers for this type of signal
        //and call them with the signal
        for(receiverMethod in receivers[signaltype]) {
            receiverMethod(signal);
        }
    }
}

Ahora tenemos un patrón que conocemos y podemos usarlo para cualquier número y tipo de clases siempre que:

  • son conscientes del RadioMast (la clase que maneja todo el paso de mensajes)
  • conocen la firma del método para enviar / recibir mensajes

Así que cambiamos la clase Radio a su forma final y simple:

class Radio {
    function ReceiveSignal(signal) {
        RadioMast.Broadcast("specialradiosignal", signal);
    }
}

Y añadimos los altavoces y el relé a la lista de receptores del RadioMast para este tipo de señal:

RadioMast.RegisterReceivers("specialradiosignal", speakers.PlaySignal);
RadioMast.RegisterReceivers("specialradiosignal", relay.RelaySignal);

Ahora la clase Speakers and Relay no tiene conocimiento de nada excepto que tienen un método que puede recibir una señal, y la clase Radio, siendo la editora, es consciente del RadioMast al que publica señales. Este es el punto de utilizar un sistema de paso de mensajes como publicar / suscribirse.


¡Realmente genial tener un ejemplo concreto que muestre cómo implementar el patrón pub / sub puede ser mejor que usar métodos 'normales'! ¡Gracias!
Maccath

1
¡De nada! Personalmente, a menudo encuentro que mi cerebro no "hace clic" cuando se trata de nuevos patrones / metodologías hasta que me doy cuenta de un problema real que me resuelve. El patrón sub / pub es excelente con arquitecturas que están estrechamente acopladas conceptualmente, pero aún queremos mantenerlas separadas tanto como sea posible. Imagina un juego en el que tienes cientos de objetos que tienen que reaccionar a las cosas que suceden a su alrededor, por ejemplo, y estos objetos pueden ser todo: jugador, bala, árbol, geometría, interfaz gráfica de usuario, etc., etc.
Anders Arpi

3
JavaScript no tiene la classpalabra clave. Por favor enfatice este hecho, ej. clasificando su código como pseudocódigo.
Rob W

De hecho, en ES6 hay una palabra clave de clase.
Minko Gechev

5

Las otras respuestas han hecho un gran trabajo al mostrar cómo funciona el patrón. Quería abordar la pregunta implícita " ¿qué hay de malo en el método antiguo? ", Ya que he estado trabajando con este patrón recientemente y encuentro que implica un cambio en mi forma de pensar.

Imagina que nos hemos suscrito a un boletín económico. El boletín publica un titular: " Baje el Dow Jones en 200 puntos ". Sería un mensaje extraño y algo irresponsable de enviar. Sin embargo, si publicó: " Enron solicitó la protección por bancarrota del capítulo 11 esta mañana ", entonces este es un mensaje más útil. Tenga en cuenta que el mensaje puede hacer que el Dow Jones caiga 200 puntos, pero eso es otro asunto.

Hay una diferencia entre enviar un comando y avisar de algo que acaba de suceder. Con esto en mente, tome su versión original del patrón pub / sub, ignorando el controlador por ahora:

$.subscribe('iquery/action/remove-order', removeOrder);

$container.on('click', '.remove_order', function(event) {
    event.preventDefault();
    $.publish('iquery/action/remove-order', $(this).parents('form:first').find('div.order'));
});

Ya existe un fuerte acoplamiento implícito aquí, entre la acción del usuario (un clic) y la respuesta del sistema (una orden que se elimina). Efectivamente, en su ejemplo, la acción está dando una orden. Considere esta versión:

$.subscribe('iquery/action/remove-order-requested', handleRemoveOrderRequest);

$container.on('click', '.remove_order', function(event) {
    event.preventDefault();
    $.publish('iquery/action/remove-order-requested', $(this).parents('form:first').find('div.order'));
});

Ahora el gestor está respondiendo a algo de interés que ha sucedido, pero no tiene la obligación de eliminar una orden. De hecho, el controlador puede hacer todo tipo de cosas que no están directamente relacionadas con la eliminación de una orden, pero que aún pueden ser relevantes para la acción de llamada. Por ejemplo:

handleRemoveOrderRequest = function(e, orders) {
    logAction(e, "remove order requested");
    if( !isUserLoggedIn()) {
        adviseUser("You need to be logged in to remove orders");
    } else if (isOkToRemoveOrders(orders)) {
        orders.last().remove();
        adviseUser("Your last order has been removed");
        logAction(e, "order removed OK");
    } else {
        adviseUser("Your order was not removed");
        logAction(e, "order not removed");
    }
    remindUserToFloss();
    increaseProgrammerBrowniePoints();
    //etc...
}

La distinción entre un comando y una notificación es una distinción útil para hacer con este patrón, en mi opinión.


Si sus últimas 2 funciones ( remindUserToFloss& increaseProgrammerBrowniePoints) estuvieran ubicadas en módulos separados, ¿publicaría 2 eventos uno después del otro allí mismo handleRemoveOrderRequesto tendría que flossModulepublicar un evento en un browniePointsmódulo cuando remindUserToFloss()haya terminado?
Bryan P

4

Para que no tenga que codificar las llamadas a métodos / funciones, simplemente publique el evento sin importar quién lo escuche. Esto hace que el editor sea independiente del suscriptor, lo que reduce la dependencia (o el acoplamiento, el término que prefiera) entre 2 partes diferentes de la aplicación.

Aquí hay algunas desventajas del acoplamiento como lo menciona wikipedia

Los sistemas estrechamente acoplados tienden a exhibir las siguientes características de desarrollo, que a menudo se consideran desventajas:

  1. Un cambio en un módulo generalmente fuerza un efecto dominó de los cambios en otros módulos.
  2. El ensamblaje de módulos puede requerir más esfuerzo y / o tiempo debido a la mayor dependencia entre módulos.
  3. Un módulo en particular puede ser más difícil de reutilizar y / o probar porque se deben incluir módulos dependientes.

Considere algo así como un objeto que encapsula datos comerciales. Tiene una llamada de método codificada para actualizar la página cada vez que se establece la edad:

var person = {
    name: "John",
    age: 23,

    setAge: function( age ) {
        this.age = age;
        showAge( age );
    }
};

//Different module

function showAge( age ) {
    $("#age").text( age );
}

Ahora no puedo probar el objeto persona sin incluir también la showAgefunción. Además, si también necesito mostrar la edad en algún otro módulo GUI, necesito codificar esa llamada al método .setAge, y ahora hay dependencias para 2 módulos no relacionados en el objeto persona. También es difícil de mantener cuando ves que se realizan esas llamadas y ni siquiera están en el mismo archivo.

Tenga en cuenta que dentro del mismo módulo, por supuesto, puede tener llamadas directas a métodos. Pero los datos comerciales y el comportamiento superficial de la interfaz gráfica de usuario no deben residir en el mismo módulo según ningún estándar razonable.


No entiendo el concepto de "dependencia" aquí; ¿Dónde está la dependencia en mi segundo ejemplo y dónde falta en el tercero? No puedo ver ninguna diferencia práctica entre mi segundo y tercer fragmento, simplemente parece agregar una nueva 'capa' entre la función y el evento sin una razón real. Probablemente estoy siendo ciego, pero creo que necesito más consejos. :(
Maccath

1
¿Podría proporcionar un caso de uso de muestra en el que publicar / suscribirse sería más apropiado que simplemente hacer una función que realice lo mismo?
Jeffrey Sweeney

@Maccath En pocas palabras: en el tercer ejemplo, no sabes o tienes que saber que removeOrderexiste, por lo que no puedes depender de él. En el segundo ejemplo, tienes que saberlo.
Esailija

Si bien todavía siento que hay mejores formas de hacer lo que describió aquí, al menos estoy convencido de que esta metodología tiene un propósito, especialmente en entornos con muchos otros desarrolladores. +1
Jeffrey Sweeney

1
@Esailija - Gracias, creo que lo entiendo un poco mejor. Entonces ... si elimino el suscriptor por completo, no se produciría un error ni nada, ¿simplemente no haría nada? ¿Y diría que esto podría ser útil en un caso en el que desee realizar una acción, pero no necesariamente sabría qué función es más relevante en el momento de la publicación, pero el suscriptor podría cambiar dependiendo de otros factores?
Maccath

1

La implementación de PubSub se ve comúnmente en donde hay:

  1. Hay una implementación similar a un portlet donde hay varios portlets que se comunican con la ayuda de un bus de eventos. Esto ayuda a crear en arquitectura aync.
  2. En un sistema estropeado por un acoplamiento estrecho, pubsub es un mecanismo que ayuda a comunicarse entre varios módulos.

Código de ejemplo -

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

  var messages = [];

  q.subscribe = function(message, fn) {
    if (!messages[message]) {
      messages[message] = [];
    }
    messages[message].push(fn);
  }

  q.publish = function(message) {
    /* fetch all the subscribers and execute*/
    if (!messages[message]) {
      return false;
    } else {
      for (var message in messages) {
        for (var idx = 0; idx < messages[message].length; idx++) {
          if (messages[message][idx])
            messages[message][idx]();
        }
      }
    }
  }
})(pubSub);

pubSub.subscribe("event-A", function() {
  console.log('this is A');
});

pubSub.subscribe("event-A", function() {
  console.log('booyeah A');
});

pubSub.publish("event-A"); //executes the methods.

1

El artículo "Las muchas caras de publicar / suscribir" es una buena lectura y una cosa que enfatizan es el desacoplamiento en tres "dimensiones". Aquí está mi resumen crudo, pero por favor haga referencia al artículo también.

  1. Desacoplamiento espacial. Las partes que interactúan no necesitan conocerse entre sí. El editor no sabe quién está escuchando, cuántos están escuchando o qué están haciendo con el evento. Los suscriptores no saben quién está produciendo estos eventos, cuántos productores hay, etc.
  2. Desacoplamiento temporal. Las partes que interactúan no necesitan estar activas al mismo tiempo durante la interacción. Por ejemplo, un suscriptor puede estar desconectado mientras un editor está publicando algunos eventos, pero puede reaccionar cuando se conecta.
  3. Desacoplamiento de sincronización. Los editores no están bloqueados mientras producen eventos y los suscriptores pueden ser notificados de forma asincrónica a través de devoluciones de llamada cada vez que llega un evento al que se han suscrito.

0

Respuesta simple La pregunta original buscaba una respuesta simple. Este es mi intento.

Javascript no proporciona ningún mecanismo para que los objetos de código creen sus propios eventos. Entonces necesitas una especie de mecanismo de eventos. el patrón Publicar / suscribirse responderá a esta necesidad, y depende de usted elegir el mecanismo que mejor se adapte a sus necesidades.

Ahora podemos ver la necesidad del patrón pub / sub, entonces, ¿preferiría manejar los eventos DOM de manera diferente a como maneja sus eventos pub / sub? En aras de reducir la complejidad y otros conceptos como la separación de preocupaciones (SoC), es posible que vea el beneficio de que todo sea uniforme.

Entonces, paradójicamente, más código crea una mejor separación de preocupaciones, lo que se adapta bien a páginas web muy complejas.

Espero que alguien considere que esta es una discusión suficientemente buena sin entrar en detalles.

Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.