105 votes

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

Un collègue m'a fait découvrir le modèle de publication et d'abonnement (en JS/jQuery), mais j'ai du mal à me faire à l'idée de pourquoi on utiliserait ce modèle plutôt que le JavaScript/jQuery "normal".

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 pourrais voir le mérite de faire ça à 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 le removeOrder fonctionnalité pour différents événements, etc.

Mais pourquoi décider de mettre en œuvre le modèle de publication/abonnement et se donner la peine de le faire, si cela revient au même ? (Pour info, j'ai utilisé 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'));
});

J'ai lu des articles sur ce modèle, mais je n'arrive pas à imaginer pourquoi cela serait nécessaire. Les tutoriels que j'ai vus qui expliquent comment pour mettre en œuvre ce modèle ne couvrent que des exemples aussi basiques que les miens.

J'imagine que l'utilité du pub/sub se révélerait dans une application plus complexe, mais je ne peux pas en imaginer une. Je crains de passer complètement à côté de l'essentiel, mais j'aimerais bien le savoir s'il y en a un !

Pouvez-vous expliquer succinctement pourquoi et dans quelles situations ce modèle est avantageux ? Est-il utile d'utiliser le modèle pub/sub pour les extraits de code comme mes exemples ci-dessus ?

229voto

Minko Gechev Points 11295

Il s'agit de couplage lâche et de responsabilité unique, ce qui va de pair avec les modèles MV* (MVC/MVP/MVVM) en JavaScript qui sont très modernes ces dernières années.

Accouplement libre est un principe orienté objet dans lequel chaque composant du système connaît sa responsabilité et ne se soucie pas des autres composants (ou du moins essaie de ne pas s'en soucier autant que possible). Le couplage lâche est une bonne chose car vous pouvez facilement réutiliser les différents modules. Vous n'êtes pas couplé aux interfaces des autres modules. En utilisant publish/subscribe, vous n'êtes couplé qu'à l'interface publish/subscribe, ce qui n'est pas très important - juste deux méthodes. Ainsi, si vous décidez de réutiliser un module dans un autre projet, vous pouvez simplement le copier-coller et il fonctionnera probablement ou, du moins, vous n'aurez pas besoin de beaucoup d'efforts pour le faire fonctionner.

Lorsque nous parlons de couplage lâche, nous devons mentionner le séparation des préoccupations . Si vous construisez une application en utilisant 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 le coupler avec la vue d'une seule application, où vous voulez le montrer, parce que généralement dans les différentes applications vous avez différentes vues. C'est donc une bonne idée d'utiliser la méthode publish/subscribe pour la communication Modèle-Vue. Lorsque votre modèle change, il publie un événement, la vue le capte et se met à jour. Vous n'avez pas de frais généraux liés à la publication/abonnement, cela vous aide à découpler. De la même manière, vous pouvez garder votre logique d'application dans le contrôleur par exemple (MVVM, MVP, ce 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), elle publie simplement un nouvel événement, le contrôleur l'attrape et décide de ce qu'il faut faire. Si vous êtes familier avec le MVC ou avec MVVM Dans les technologies Microsoft (WPF/Silverlight), vous pouvez considérer le processus de publication/abonnement comme le processus suivant Modèle d'observateur . Cette approche est utilisée dans des frameworks comme 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 d'un peu différent (il y a une intersection entre celle que je vais décrire ensuite et la dernière mentionnée). Il suffit de structurer votre application en différents modules. Prenons l'exemple de Twitter.

Twitter Modules

Si vous regardez l'interface, vous avez simplement des boîtes différentes. Vous pouvez considérer chaque boîte comme un module différent. Par exemple, vous pouvez publier un tweet. Cette action nécessite la mise à jour de quelques modules. Tout d'abord, elle doit mettre à jour les données de votre profil (case supérieure gauche), mais aussi votre ligne de temps. 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 mieux) de publier simplement 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 des 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 (ce n'est pas le code original de Twitter, c'est juste un échantillon de ma part) :

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, il y a un excellent exposé par 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 publish/subscribe. Si vous avez des centaines d'événements, il peut devenir très difficile de les gérer tous. Vous pouvez également avoir des collisions si vous n'utilisez pas l'espacement des noms (ou si vous ne l'utilisez pas de la bonne manière). Vous trouverez ici une implémentation avancée du Mediator qui ressemble beaucoup à un publish/subscribe. https://github.com/ajacksified/Mediator.js . Il dispose de l'espacement des noms et de fonctionnalités telles que le "bouillonnement" des événements qui, bien sûr, peut être interrompu. Un autre inconvénient de publish/subscribe est la difficulté des tests unitaires, il peut devenir difficile d'isoler les différentes fonctions dans les modules et de les tester indépendamment.

3 votes

Merci, cela a du sens. Je connais bien le modèle MVC car je l'utilise tout le temps en PHP, mais je n'y avais pas pensé en termes de programmation événementielle :)

2 votes

Merci pour cette description. Elle m'a vraiment aidé à comprendre le concept.

1 votes

C'est une excellente réponse. Je n'ai pas pu m'empêcher de la voter :)

16voto

Anders Holmström Points 4422

L'objectif principal est de réduire le couplage entre les codes. C'est un mode de pensée quelque peu événementiel, 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 à du JavaScript.

Disons que nous avons une classe Radio et une 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 qu'une radio reçoit un signal, on veut qu'un certain nombre de relais relaient le message d'une manière ou d'une autre. Le nombre et les types de relais peuvent varier. On peut procéder comme suit :

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 voulons qu'un autre composant prenne également part aux signaux que la classe Radio reçoit, à savoir les haut-parleurs :

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

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

Nous pourrions répéter le même schéma :

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 encore améliorer la situation en créant une interface, comme "SignalListener", de sorte que nous n'ayons besoin que d'une seule liste dans la classe Radio, et que nous puissions toujours appeler la même fonction sur n'importe quel objet qui souhaite écouter le signal. Mais cela crée toujours un couplage entre l'interface/la classe de base/etc que nous choisissons et la classe Radio. En fait, chaque fois que vous modifiez l'une des classes Radio, Signal ou Relais, vous devez penser à la façon dont cela pourrait affecter les deux autres classes.

Maintenant, essayons quelque chose de différent. Créons une quatrième classe appelé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 que nous connaissons et nous pouvons l'utiliser pour n'importe quel nombre et type de classes, tant que celles-ci :

  • sont conscients de la RadioMast (la classe qui gère le passage des messages)
  • connaissent la signature de la méthode d'envoi/réception des messages

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

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

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

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

Maintenant, les classes Speakers et Relay n'ont aucune connaissance de quoi que ce soit, sauf qu'elles ont une méthode qui peut recevoir un signal, et la classe Radio, étant l'éditeur, est consciente de la RadioMast à laquelle elle publie des signaux. C'est l'intérêt d'utiliser un système de passage de messages comme publish/subscribe.

0 votes

C'est vraiment génial d'avoir un exemple concret qui montre comment la mise en œuvre du modèle pub/sub peut être meilleure que l'utilisation de méthodes " normales " ! Merci à vous !

1 votes

Vous êtes les bienvenus ! Personnellement, je trouve souvent que mon cerveau ne "clique" pas lorsqu'il s'agit de nouveaux modèles/méthodologies jusqu'à ce que je réalise un problème réel qu'il résout pour moi. Le modèle sub/pub est idéal pour les architectures qui sont étroitement couplées d'un point de vue conceptuel, mais qui doivent rester séparées autant que possible. Imaginez un jeu où vous avez des centaines d'objets qui doivent tous réagir aux choses qui se passent autour d'eux, par exemple, et ces objets peuvent être n'importe quoi : joueur, balle, arbre, géométrie, interface, etc. etc.

3 votes

JavaScript n'a pas le class mot-clé. Veuillez souligner ce fait, par exemple en classant votre code comme pseudo-code.

5voto

John Trevithick Points 2141

Les autres réponses ont fait un excellent travail en montrant comment le modèle fonctionne. Je voulais répondre à la question implicite " Qu'est-ce qui ne va pas avec l'ancienne méthode ? "J'ai travaillé sur ce modèle récemment, et je trouve qu'il implique un changement dans ma façon de penser.

Imaginons que nous soyons abonnés à un bulletin économique. Le bulletin publie un titre : " Faire baisser le Dow Jones de 200 points ". Ce serait un message étrange et quelque peu irresponsable à envoyer. Si par contre, il publiait : " Enron s'est placé sous la protection du chapitre 11 de la loi sur les faillites ce matin. ", il s'agit alors d'un message plus utile. Notez que le message peut cause le Dow Jones à chuter de 200 points, mais c'est une autre affaire.

Il y a une différence entre envoyer un ordre et informer de quelque chose qui vient de se produire. En gardant cela à l'esprit, reprenez 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 couplage fort implicite entre l'action de l'utilisateur (un clic) et la réponse du système (la suppression d'une commande). Effectivement, dans votre exemple, l'action consiste à donner 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 produit, mais n'est pas obligé de retirer un ordre. En fait, le gestionnaire peut faire toutes sortes de choses qui ne sont pas directement liées au retrait d'un ordre, mais qui peuvent néanmoins être pertinentes pour l'action appelante. 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.

0 votes

Si vos 2 dernières fonctions ( remindUserToFloss & increaseProgrammerBrowniePoints ) étaient situés dans des modules distincts, publieriez-vous deux événements l'un après l'autre, là, dans le module handleRemoveOrderRequest ou auriez-vous un flossModule publier un événement dans un browniePoints module lorsque remindUserToFloss() est fait ?

4voto

Esailija Points 74052

Pour ne pas avoir à coder en dur des appels de méthodes ou de fonctions, il suffit de publier l'événement sans se soucier de qui l'écoute. L'éditeur est ainsi indépendant de l'abonné, ce qui réduit la dépendance (ou le couplage, selon le terme que vous préférez) entre deux parties différentes de l'application.

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

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

  1. Une modification d'un module entraîne généralement un effet d'entraînement sur d'autres modules.
  2. L'assemblage des modules peut demander 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 parce que des modules dépendants doivent être inclus.

Considérez quelque chose comme un objet encapsulant des données commerciales. Il a une méthode codée en dur pour mettre à jour la page chaque fois que l'âge est fixé :

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 l'objet showAge fonction. De même, si j'ai besoin d'afficher l'âge dans un autre module GUI également, je dois coder en dur l'appel de cette méthode dans .setAge et maintenant il y a des dépendances pour 2 modules non liés dans l'objet personne. C'est aussi juste difficile à maintenir quand on voit que ces appels sont faits et qu'ils ne sont même pas dans le même fichier.

Notez qu'à l'intérieur d'un même module, vous pouvez bien sûr avoir des appels directs de méthodes. Mais les données commerciales et le comportement superficiel superficielles ne devraient pas résider dans le même module, selon toute norme raisonnable.

0 votes

Je ne comprends pas le concept de "dépendance" ; où est la dépendance dans mon deuxième exemple, et où est-elle absente dans mon troisième ? Je ne vois aucune différence pratique entre mon deuxième et mon troisième exemple - il semble simplement ajouter une nouvelle "couche" entre la fonction et l'événement sans véritable raison. Je suis probablement aveugle, mais je pense que j'ai besoin de plus d'indications :(

1 votes

Pourriez-vous fournir un exemple de cas d'utilisation où la publication et l'abonnement seraient plus appropriés que la création d'une fonction qui effectue la même chose ?

0 votes

@Maccath En termes simples : dans le troisième exemple, vous ne savez pas ou ne devez pas savoir que removeOrder n'existe même pas, donc vous ne pouvez pas en être dépendant. Dans le second exemple, vous devez savoir.

1voto

user2756335 Points 99

L'implémentation de PubSub est communément vue dans les cas où il y a -

  1. Il y a une implémentation de type portlet où il y a plusieurs portlets qui communiquent avec l'aide d'un bus d'événements. Cela permet de créer une architecture aync.
  2. Dans un système marqué par un couplage étroit, pubsub est un mécanisme qui aide à communiquer entre les 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("event-A"); //executes the methods.

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X