Pourquoi utiliserait-on le modèle Publish / Subscribe (dans JS / jQuery)?


103

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 sur 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 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 removeOrderfonctionnalité pour différents événements, etc.

Mais pourquoi décideriez-vous d'implémenter le modèle de publication / d'abonnement et d'aller aux longueurs suivantes, s'il fait la même chose? (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 sur le modèle à coup sûr, mais je ne peux pas imaginer pourquoi cela serait jamais nécessaire. Les tutoriels que j'ai vus qui expliquent comment implémenter ce modèle ne couvrent que des exemples aussi basiques que le mien.

J'imagine que l'utilité du pub / sub se manifesterait dans une application plus complexe, mais je ne peux pas en imaginer une. J'ai peur de manquer 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 / sub pour les extraits de code comme mes exemples ci-dessus?

Réponses:


222

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

Le couplage lâche 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). 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. En utilisant la publication / l'abonnement, vous êtes uniquement associé à l'interface de publication / abonnement, ce qui n'est pas un gros problème - juste deux méthodes. Donc, si vous décidez de réutiliser un module dans un projet différent, vous pouvez simplement le copier et le coller et cela fonctionnera probablement ou du moins vous n'aurez pas besoin de beaucoup d'efforts pour le faire fonctionner.

Quand on parle de couplage lâche, il faut mentionner la séparation des préoccupations. Si vous créez une application à l'aide d'un modèle architectural MV *, vous disposez toujours d'un ou plusieurs modèles et d'une ou plusieurs vues. Le modèle est la partie métier de l'application. Vous pouvez le réutiliser dans différentes applications, donc ce n'est pas une bonne idée de le coupler avec la vue d'une seule application, là où vous voulez l'afficher, car généralement dans les différentes applications, vous avez des vues différentes. C'est donc une bonne idée d'utiliser la publication / l'abonnement pour la communication Model-View. Lorsque votre modèle change, il publie un événement, la vue le capture et se met à jour. Vous n'avez aucune surcharge de publication / abonnement, cela vous aide pour le découplage. De la même manière, vous pouvez conserver la logique de votre 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), il publie simplement un nouvel événement, le contrôleur l'attrape et décide quoi faire. Si vous connaissez leModèle MVC ou avec MVVM dans les technologies Microsoft (WPF / Silverlight), vous pouvez considérer la publication / l'abonnement comme le modèle Observer . 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 d'un peu différent (il y a une intersection entre celle que je décrirai ensuite et la dernière mentionnée). Structurez simplement votre application en différents modules. Par exemple, regardez Twitter.

Modules Twitter

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 publier un tweet. Cette action nécessite la mise à jour de quelques modules. Tout d'abord, il doit mettre à jour vos données de profil (case en haut à gauche) mais il doit également mettre à jour votre chronologie. Bien sûr, vous pouvez conserver les 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 (ce n'est pas du code Twitter original, c'est juste un exemple 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 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 devenir très déroutant de les gérer tous. Vous pouvez également avoir des collisions si vous n'utilisez pas d'espacement de noms (ou si vous ne l'utilisez pas de la bonne manière). Une implémentation avancée de Mediator qui ressemble beaucoup à une publication / abonnement peut être trouvée ici https://github.com/ajacksified/Mediator.js . Il a un espace de noms et des fonctionnalités telles que le «bouillonnement» d'événement qui, bien sûr, peut être interrompu. Un autre inconvénient de publier / souscrire est le test unitaire dur, il peut devenir difficile d'isoler les différentes fonctions dans les modules et de les tester indépendamment.


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

2
Merci pour cette description. M'a vraiment aidé à comprendre le concept.
flybear

1
C'est une excellente réponse. Je ne pouvais pas m'empêcher de voter pour ça :)
Naveed Butt

1
Excellente explication, plusieurs exemples, d'autres suggestions de lecture. A ++.
Carson

16

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

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

Disons que nous avons une radio de classe et un relais de classe:

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 relaient 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 très bien. Mais maintenant, imaginez que nous voulons qu'un composant différent prenne également part aux signaux que la classe Radio reçoit, à savoir les haut-parleurs:

(désolé si les analogies ne sont pas de premier ordre ...)

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

Nous pourrions répéter le modèle à 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 rendre cela encore meilleur 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 que nous avons qui veut é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. Fondamentalement, chaque fois que vous changez l'une des classes Radio, Signal ou Relay, vous devez réfléchir à la façon dont cela pourrait éventuellement 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);
        }
    }
}

Nous avons maintenant un modèle dont nous sommes conscients et nous pouvons l'utiliser pour n'importe quel nombre et types de classes tant qu'ils:

  • sont conscients du RadioMast (la classe gérant tous les messages passant)
  • sont conscients de la signature de la méthode pour envoyer / recevoir des messages

Nous changeons donc la classe Radio dans 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 des récepteurs de RadioMast pour ce type de signal:

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

Maintenant, la classe Speakers and Relay n'a aucune connaissance de quoi que ce soit, sauf qu'elle a une méthode qui peut recevoir un signal, et la classe Radio, en tant qu'éditeur, est consciente du RadioMast sur lequel elle publie des signaux. C'est le point d'utiliser un système de transmission de messages tel que publier / s'abonner.


Vraiment génial d'avoir un exemple concret qui montre à quel point l'implémentation du modèle pub / sub peut être meilleure que l'utilisation de méthodes «normales»! Je vous remercie!
Maccath

1
Vous êtes les bienvenus! Personnellement, je trouve souvent que mon cerveau ne «clique» pas quand 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 excellent avec des architectures qui sont étroitement couplées conceptuellement, mais nous voulons toujours les garder 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 tout: joueur, balle, arbre, géométrie, interface graphique, etc.
Anders Arpi

3
JavaScript n'a pas le classmot - clé. Veuillez souligner ce fait, par exemple. en classant votre code en pseudo-code.
Rob W

En fait, dans ES6, il y a un mot-clé de classe.
Minko Gechev

5

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

Imaginez que nous nous sommes abonnés à un bulletin économique. Le bulletin publie un titre: " Baisser le Dow Jones de 200 points ". Ce serait un message étrange et quelque peu irresponsable à envoyer. Si toutefois, il a publié: " Enron a demandé la protection contre la faillite du chapitre 11 ce matin ", alors c'est un message plus utile. Notez que le message peut faire chuter le Dow Jones de 200 points, mais c'est une autre affaire.

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 le moment:

$.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 y a déjà ici un couplage fort implicite, entre l'action utilisateur (un clic) et la réponse système (une commande en cours de suppression). Efficacement 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 à un événement intéressant qui s'est produit, mais n'est pas obligé de retirer une commande. En fait, le gestionnaire peut faire toutes sortes de choses qui ne sont pas directement liées à la suppression d'une commande, mais qui peuvent toujours ê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.


si vos 2 dernières fonctions ( remindUserToFloss& increaseProgrammerBrowniePoints) étaient situées dans des modules séparés, publieriez-vous 2 événements l'un après l'autre juste là handleRemoveOrderRequestou auriez-vous une flossModulepublication d'un événement dans un browniePointsmodule lorsque cela remindUserToFloss()est fait?
Bryan P

4

Pour ne pas avoir à coder en dur les appels de méthode / fonction, il vous suffit de publier l'événement sans vous soucier de qui écoute. Cela rend l'éditeur indépendant de l'abonné, ce qui réduit la dépendance (ou le couplage, quel que soit le terme que vous préférez) entre 2 parties différentes de l'application.

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

Les systèmes étroitement couplés ont tendance à présenter les caractéristiques de développement suivantes, qui sont 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 d'autres modules.
  2. L'assemblage de 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érez 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 showAgefonction. De plus, si j'ai besoin d'afficher l'âge dans un autre module d'interface graphique, je dois coder en dur cet appel de méthode .setAge, et maintenant il y a 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 effectués et qu'ils ne sont même pas dans le même fichier.

Notez qu'à l'intérieur du même module, vous pouvez bien sûr avoir des appels de méthode directs. Mais les données commerciales et le comportement superficiel de l'interface graphique ne devraient pas résider dans le même module selon des normes raisonnables.


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

1
Pourriez-vous fournir un exemple de cas d'utilisation où publier / souscrire serait plus approprié que de simplement créer une fonction qui effectue la même chose?
Jeffrey Sweeney

@Maccath En termes simples: dans le troisième exemple, vous ne savez pas ou devez savoir que removeOrdercela existe même, vous ne pouvez donc pas en dépendre. Dans le deuxième exemple, vous devez savoir.
Esailija

Même si j'ai toujours l'impression qu'il existe de meilleures façons de faire ce que vous avez décrit ici, je suis au moins convaincu que cette méthodologie a un but, en particulier dans les environnements avec de nombreux autres développeurs. +1
Jeffrey Sweeney

1
@Esailija - Merci, je pense que je comprends un peu mieux. Donc ... si je supprimais complètement l'abonné, ce ne serait pas une erreur ou quoi que ce soit, cela ne ferait simplement rien? Et diriez-vous que cela pourrait être utile dans un cas où vous souhaitez effectuer une action, mais ne sauriez pas nécessairement quelle fonction est la plus pertinente au moment de la publication, mais l'abonné pourrait changer en fonction d'autres facteurs?
Maccath

1

La mise en œuvre de PubSub est généralement observée là où il y a -

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

1

Le papier «Les nombreux visages de publier / souscrire» est une bonne lecture et une chose sur laquelle ils insistent est le découplage en trois «dimensions». Voici mon résumé grossier, mais veuillez également faire référence au document.

  1. Découplage de l'espace. Les parties en interaction n'ont pas besoin de se connaître. L'éditeur ne sait pas qui écoute, combien écoutent, ni ce qu'ils font de l'événement. Les abonnés ne savent pas qui produit ces événements, combien il y a de producteurs, etc.
  2. Découplage temporel. Les parties en interaction n'ont pas besoin d'être actives en même temps pendant l'interaction. Par exemple, un abonné peut être déconnecté pendant qu'un éditeur publie certains événements, mais il peut y réagir lorsqu'il est en ligne.
  3. Découplage de 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 via des rappels chaque fois qu'un événement auquel ils se sont abonnés arrive.

0

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 / S'abonner répondra à ce besoin, et c'est à vous de choisir le mécanisme qui correspond le mieux à vos propres besoins.

Maintenant, nous pouvons voir un besoin pour le modèle pub / sub, alors préférez-vous gérer les événements DOM différemment de la façon dont vous gérez vos événements pub / sub? Dans le but de réduire la complexité et d'autres concepts tels que la séparation des préoccupations (SoC), vous pourriez voir l'avantage que tout soit uniforme.

Donc, paradoxalement, plus de code crée une meilleure séparation des préoccupations, qui évolue bien jusqu'à des pages Web très complexes.

J'espère que quelqu'un trouvera cette discussion assez bonne sans entrer dans les détails.

En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.