web-dev-qa-db-fra.com

Pourquoi utiliser le modèle Publish / Subscribe (dans JS / jQuery)?

Donc, un collègue m'a présenté le modèle de publication/abonnement (dans JS/jQuery), mais j'ai du mal à comprendre pourquoi on utiliserait ce modèle au lieu de "normal" JavaScript/jQuery.

Par exemple, auparavant, j'avais le code suivant ...

$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();
    }
});

Et je pouvais voir le mérite de faire cela à la place, par exemple ...

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'));
});

Parce qu'il introduit la possibilité de réutiliser la fonctionnalité removeOrder pour différents événements, etc.

Mais pourquoi voudriez-vous mettre en œuvre le modèle de publication/abonnement et aller aux longueurs suivantes, si cela fait la même chose? (Pour info, j'ai utilisé jQuery minuscule 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'));
});

J'ai lu sur le modèle à coup sûr, mais je ne peux tout simplement pas imaginer pourquoi cela serait nécessaire. Les tutoriels que j'ai vus expliquent comment pour implémenter ce modèle ne couvrent que des exemples aussi élémentaires que le mien.

J'imagine que l'utilité de pub/sub apparaîtrait dans une application plus complexe, mais je ne peux pas en imaginer une. Je crains que je manque complètement le point; mais j'aimerais savoir le point s'il y en a un!

Pourriez-vous expliquer succinctement pourquoi et dans quelles situations ce modèle est avantageux? Vaut-il la peine d’utiliser le modèle pub/sous pour les extraits de code, comme dans mes exemples ci-dessus?

97
Maccath

Tout est question de couplage lâche et de responsabilité unique, ce qui va de pair avec les modèles JavaScript * MV * (MVC/MVP/MVVM), qui sont très modernes au cours des dernières années.

Couplage faible est un principe orienté objet dans lequel chaque composant du système connaît ses responsabilités et ne se soucie pas des autres composants (ou au moins essaie de ne pas se soucier d’eux autant que possible). Un couplage lâche est une bonne chose car vous pouvez facilement réutiliser les différents modules. Vous n'êtes pas couplé avec les interfaces d'autres modules. L’utilisation de Publier/Abonné n’est couplée qu’à l’interface Publier/Abonner, ce qui n’a rien de grave: deux méthodes seulement. Par conséquent, si vous décidez de réutiliser un module dans un projet différent, vous pouvez simplement le copier et le coller. Cela fonctionnera probablement ou au moins vous n'aurez pas besoin de beaucoup d’efforts pour le faire fonctionner.

Quand on parle de couplage lâche, il convient de mentionner séparation des préoccupations . Si vous construisez une application à l’aide d’un modèle architectural MV *, vous avez toujours un ou plusieurs modèles et une ou plusieurs vues. Le modèle est la partie commerciale de l'application. Vous pouvez le réutiliser dans différentes applications. Ce n’est donc pas une bonne idée de l’associer à la vue d’une seule application, où vous souhaitez l’afficher, car vous avez généralement des vues différentes dans les différentes applications. Il est donc judicieux d’utiliser Publier/Souscrire pour la communication Model-View. Lorsque votre modèle change, il publie un événement, la vue l'attrape et se met à jour. La publication/abonnement ne génère pas de temps système, cela vous aide pour le découplage. De la même manière, vous pouvez conserver votre logique d’application dans le contrôleur par exemple (MVVM, MVP n’est pas exactement un contrôleur) et garder la vue aussi simple que possible. Lorsque votre vue change (ou que l'utilisateur clique sur quelque chose, par exemple), il ne fait que publier un nouvel événement, le contrôleur l'attrape et décide quoi faire. Si vous connaissez le modèle MVC ou avec MVVM dans les technologies Microsoft (WPF/Silverlight) vous pouvez penser à la publication/abonnement comme le modèle d'observateur . Cette approche est utilisée dans des frameworks tels que Backbone.js, Knockout.js (MVVM).

Voici un exemple:

//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);
      }
   }
}());

Un autre exemple. Si vous n’aimez pas l’approche MV *, vous pouvez utiliser quelque chose de légèrement différent (il ya une intersection entre celle que je décrirai ensuite et celle mentionnée en dernier). Il suffit de structurer votre application dans différents modules. Par exemple, regardez sur Twitter.

Twitter Modules

Si vous regardez l'interface, vous avez simplement différentes cases. Vous pouvez considérer chaque boîte comme un module différent. Par exemple, vous pouvez poster un Tweet. Cette action nécessite la mise à jour de quelques modules. Tout d'abord, il doit mettre à jour les données de votre profil (zone supérieure gauche), mais également votre calendrier. Bien sûr, vous pouvez conserver des références aux deux modules et les mettre à jour séparément à l’aide de leur interface publique, mais il est plus facile (et meilleur) de simplement publier un événement. Cela facilitera la modification de votre application en raison d'un couplage plus lâche. Si vous développez un nouveau module qui dépend de nouveaux tweets, vous pouvez simplement vous abonner à l'événement "publish-Tweet" et le gérer. Cette approche est très utile et peut rendre votre application très découplée. Vous pouvez réutiliser vos modules très facilement.

Voici un exemple de base de la dernière approche (il ne s’agit pas d’un code Twitter original, c’est juste un exemple pour moi):

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);
           });
       }
   };
}());

Pour cette approche, l'excellent discours de Nicholas Zakas . Pour l'approche MV *, les meilleurs articles et livres que je connaisse sont publiés par Addy Osmani .

Inconvénients: vous devez faire attention à l'utilisation excessive de publication/abonnement. Si vous avez des centaines d’événements, il peut être très difficile de les gérer tous. Vous pouvez également avoir des collisions si vous n’utilisez pas l’espacement de noms (ou si vous ne l’utilisez pas correctement). Une implémentation avancée de Mediator qui ressemble beaucoup à une publication/souscription peut être trouvée ici https://github.com/ajacksified/Mediator.js . Il possède un espacement de noms et des fonctionnalités telles que l’événement "bouillonnant" qui, bien entendu, peuvent être interrompues. Un autre inconvénient de la publication/abonnement est le test unitaire, il peut devenir difficile d’isoler les différentes fonctions des modules et de les tester indépendamment.

218
Minko Gechev

L'objectif principal est de réduire le couplage entre le code. C'est une façon de penser basée sur les événements, mais les "événements" ne sont pas liés à un objet spécifique.

Je vais écrire un gros exemple ci-dessous dans un pseudo-code qui ressemble un peu à JavaScript.

Disons que nous avons une classe Radio et un classe Relais:

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?
    }
}

Chaque fois que la radio reçoit un signal, nous voulons qu'un certain nombre de relais transmettent le message d'une manière ou d'une autre. Le nombre et les types de relais peuvent différer. Nous pourrions le faire comme ceci:

class Radio {
    var relayList = [];

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

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

}

Cela fonctionne bien. Mais imaginons maintenant que nous souhaitons qu'un autre composant prenne également partie des signaux reçus par la classe de radio, à savoir les enceintes:

(désolé si les analogies ne sont pas top notch ...)

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

Nous pourrions répéter le motif à nouveau:

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);
        }

    }

}

Nous pourrions améliorer encore cela en créant une interface, telle que "SignalListener", de sorte que nous n’ayons besoin que d’une liste dans la classe Radio et que nous puissions toujours appeler la même fonction sur l’objet que nous avons qui veut écouter le signal. Mais cela crée toujours un couplage entre l’interface/la classe de base/etc. choisie et la classe Radio. En gros, chaque fois que vous changez de classe dans la classe Radio, Signal ou Relais, vous devez réfléchir à la manière dont cela pourrait affecter les deux autres classes.

Essayons maintenant quelque chose de différent. Créons une quatrième classe nommée 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);
        }
    }
}

Maintenant nous avons un motif dont nous sommes conscients et nous pouvons l’utiliser pour n’importe quel nombre et types de classes à condition qu’ils:

  • sont au courant de RadioMast (la classe qui gère tous les messages qui passent)
  • sont au courant de la signature de la méthode d'envoi/réception de messages

Nous changeons donc la classe Radio en sa forme finale et simple:

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

Et nous ajoutons les haut-parleurs et le relais à la liste de récepteurs de RadioMast pour ce type de signal:

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

Désormais, les classes Speakers et Relay n'ont aucune connaissance, sauf qu'elles disposent d'une méthode permettant de recevoir un signal, et la classe Radio, en tant qu'éditeur, connaît le type de message que RadioMast publie. C'est le point d'utiliser un système de transmission de messages tel que publier/s'abonner.

15
Anders Arpi

Les autres réponses ont fait un excellent travail en montrant comment fonctionne le modèle. Je voulais aborder la question implicite " qu'est-ce qui ne va pas avec l'ancienne méthode? " car j'ai déjà travaillé avec cela motif récemment, et je trouve que cela implique un changement de pensée.

Imaginons que nous ayons souscrit à un bulletin économique. Le bulletin publie un titre: " Abaissez le Dow Jones de 200 points ". Ce serait un message étrange et quelque peu irresponsable à envoyer. Si toutefois, il publiait: " Enron a demandé ce matin la protection de la faillite du chapitre 11 ", ce message est donc plus utile. Notez que le message peut faire chuter le Dow Jones de 200 points, mais c’est un autre problème.

Il y a une différence entre envoyer une commande et signaler quelque chose qui vient de se passer. Dans cet esprit, prenez votre version originale du modèle pub/sub en ignorant le gestionnaire pour l'instant:

$.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'));
});

Il existe déjà un fort couplage implicite ici, entre l'action utilisateur (un clic) et la réponse système (une commande en cours de suppression). Effectivement, dans votre exemple, l’action donne une commande. Considérez cette version:

$.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'));
});

Maintenant, le gestionnaire répond à quelque chose d'intéressant qui s'est passé, mais n'est pas obligé de supprimer une commande. En fait, le gestionnaire peut faire toutes sortes de choses qui ne sont pas directement liées à la suppression d'un ordre, mais qui peuvent néanmoins être pertinentes pour l'action d'appel. Par exemple:

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 distinction entre une commande et une notification est une distinction utile à faire avec ce modèle, IMO.

5
Trevedhek

Pour que vous n'ayez pas à coder en dur les appels de méthode/fonction, vous publiez simplement l'événement sans se soucier de savoir qui l'écoute. Cela rend l'éditeur indépendant de l'abonné, réduisant ainsi la dépendance (ou le couplage, quel que soit le terme de votre choix) entre 2 parties différentes de l'application.

Voici quelques inconvénients du couplage mentionnés par wikipedia

Les systèmes à couplage étroit ont tendance à présenter les caractéristiques de développement suivantes, souvent considérées comme des inconvénients:

  1. Un changement dans un module force généralement un effet d'entraînement des changements dans les autres modules.
  2. L’assemblage des modules peut nécessiter plus d’efforts et/ou de temps en raison de la dépendance accrue entre les modules.
  3. Un module particulier peut être plus difficile à réutiliser et/ou à tester car des modules dépendants doivent être inclus.

Considérons quelque chose comme un objet encapsulant des données d’entreprise. Il a un appel de méthode codé en dur pour mettre à jour la page chaque fois que l’âge est défini:

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

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

//Different module

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

Maintenant, je ne peux pas tester l'objet personne sans inclure également la fonction showAge. De plus, si j’ai besoin de montrer l’âge dans un autre module graphique, j’ai besoin de coder en dur cet appel de méthode dans .setAge, et maintenant, il existe des dépendances pour 2 modules non liés dans l'objet personne. Il est également difficile à maintenir lorsque vous voyez que ces appels sont passés et qu’ils ne sont même pas dans le même fichier.

Notez que dans le même module, vous pouvez bien sûr avoir des appels de méthode directs. Mais les données commerciales et le comportement d'interface graphique superficiel ne doivent pas résider dans le même module selon des critères raisonnables.

4
Esailija

Le document "Les nombreuses facettes de l'édition/abonnement" est une bonne lecture et une des choses sur lesquelles ils insistent est la décomposition en trois "dimensions". Voici mon résumé brut, mais s'il vous plaît voir le papier à la place.

  1. Espace en déclin. Les parties en interaction n'ont pas besoin de se connaître. L'éditeur ne sait pas qui écoute, combien ou ce qu'ils font avec l'événement. Les abonnés ne savent pas qui produit ces événements ni combien de producteurs il y a, etc.
  2. Découpage du temps. Il n'est pas nécessaire que les parties en interaction soient actives en même temps pendant l'interaction. Par exemple. un abonné peut être déconnecté pendant la publication de certains événements par un éditeur; il peut y réagir lorsqu'il devient en ligne.
  3. Découplage de la synchronisation. Les éditeurs ne sont pas bloqués lors de la production d'événements et les abonnés peuvent être notifiés de manière asynchrone par le biais de rappels chaque fois qu'un événement auquel ils se sont abonnés arrive.
0
Peheje

L'implémentation de PubSub est souvent observée dans les endroits où il y a -

  1. Il existe un portlet de type implémentation dans lequel plusieurs portlets communiquent à l'aide d'un bus d'événements. Cela aide à créer une architecture aync.
  2. Dans un système gâché par un couplage étroit, pubsub est un mécanisme qui facilite la communication entre différents modules.

Exemple de code -

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("A"); //executes the methods.
0
user2756335

Réponse simple La question initiale cherchait une réponse simple. Voici ma tentative.

Javascript ne fournit aucun mécanisme permettant aux objets de code de créer leurs propres événements. Vous avez donc besoin d’une sorte de mécanisme d’événement. Le modèle Publier/Souscrire répondra à ce besoin et c'est à vous de choisir le mécanisme qui répond le mieux à vos besoins.

Nous pouvons maintenant voir un besoin pour le modèle pub/sub. Dans ce cas, auriez-vous plutôt à gérer les événements DOM différemment de la façon dont vous gérez vos événements pub/sous? Par souci de réduction de la complexité et d'autres concepts tels que la séparation des préoccupations (SoC), vous pouvez voir l'avantage de l'uniformité.

Donc, paradoxalement, plus de code crée une meilleure séparation des problèmes, ce qui peut aller jusqu’à des pages Web très complexes.

J'espère que quelqu'un trouvera cela assez bon pour discuter sans entrer dans les détails.

0
Simon Miller