it-swarm.com.de

Warum sollte man das Publish / Subscribe-Muster (in JS / jQuery) verwenden?

Ein Kollege hat mich also mit dem Publish/Subscribe-Muster (in JS/jQuery) bekannt gemacht, aber es fällt mir schwer, mich mit warum auseinanderzusetzen. Man würde dieses Muster über "normal" setzen. JavaScript/jQuery.

Zum Beispiel hatte ich vorher den folgenden Code ...

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

Und ich könnte zum Beispiel den Vorzug sehen, dies zu tun ...

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

Weil es die Möglichkeit bietet, die removeOrder -Funktionalität für verschiedene Ereignisse usw. wiederzuverwenden.

Aber warum sollten Sie das Publish/Subscribe-Muster implementieren und die folgenden Schritte ausführen, wenn es dasselbe tut? (Zu Ihrer Information, ich habe 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'));
});

Ich habe mit Sicherheit über das Muster gelesen, kann mir aber nicht vorstellen, warum dies jemals notwendig sein würde. Die Tutorials, die ich gesehen habe, erklären wie, um dieses Muster zu implementieren, decken nur ebenso grundlegende Beispiele ab wie meine eigenen.

Ich stelle mir vor, dass sich die Nützlichkeit des Pubs/Sub in einer komplexeren Anwendung bemerkbar machen würde, aber ich kann mir keine vorstellen. Ich habe Angst, dass ich den Punkt völlig verfehle; aber ich würde gerne wissen, ob es einen gibt!

Können Sie prägnant erklären, warum und in welchen Situationen dieses Muster vorteilhaft ist? Lohnt es sich, das Pub/Sub-Muster für Code-Snippets wie in den obigen Beispielen zu verwenden?

97
Maccath

Es dreht sich alles um lose Kopplung und Einzelverantwortung, was mit MV * -Mustern (MVC/MVP/MVVM) in JavaScript einhergeht, die in den letzten Jahren sehr modern waren.

lose Kopplung ist ein objektorientiertes Prinzip, bei dem jede Komponente des Systems ihre Verantwortung kennt und sich nicht um die anderen Komponenten kümmert (oder zumindest versucht, sich nicht so weit wie möglich darum zu kümmern). Eine lose Kopplung ist eine gute Sache, da Sie die verschiedenen Module problemlos wiederverwenden können. Sie sind nicht mit den Schnittstellen anderer Module gekoppelt. Wenn Sie Publish/Subscribe verwenden, sind Sie nur mit der Publish/Subscribe-Schnittstelle verbunden, was keine große Sache ist - nur mit zwei Methoden. Wenn Sie also ein Modul in einem anderen Projekt wiederverwenden möchten, können Sie es einfach kopieren und einfügen. Es wird wahrscheinlich funktionieren, oder Sie müssen sich nicht viel Mühe geben, damit es funktioniert.

Wenn wir von loser Kopplung sprechen, sollten wir das Trennung von Bedenken erwähnen. Wenn Sie eine Anwendung mit einem MV * -Architekturmuster erstellen, verfügen Sie immer über ein Modell und eine Ansicht. Das Modell ist der geschäftliche Teil der Anwendung. Sie können es in verschiedenen Anwendungen wiederverwenden. Daher ist es keine gute Idee, es mit der Ansicht einer einzelnen Anwendung zu koppeln, in der Sie es anzeigen möchten, da Sie normalerweise in den verschiedenen Anwendungen unterschiedliche Ansichten haben. Es ist daher eine gute Idee, Publish/Subscribe für die Model-View-Kommunikation zu verwenden. Wenn Ihr Modell ein Ereignis ändert, wird es von der Ansicht abgefangen und aktualisiert. Sie haben keinen Overhead beim Publizieren/Abonnieren, es hilft Ihnen beim Entkoppeln. Auf die gleiche Weise können Sie beispielsweise Ihre Anwendungslogik im Controller behalten (MVVM, MVP ist nicht genau ein Controller) und die Ansicht so einfach wie möglich halten. Wenn sich Ihre Ansicht ändert (oder der Benutzer zum Beispiel auf etwas klickt), wird nur ein neues Ereignis veröffentlicht, der Controller fängt es ab und entscheidet, was zu tun ist. Wenn Sie mit dem Muster MVC oder mit MVVM in Microsoft-Technologien (WPF) vertraut sind/Silverlight) Sie können sich das Publizieren/Abonnieren wie das Beobachter-Muster vorstellen. Dieser Ansatz wird in Frameworks wie Backbone.js, Knockout.js (MVVM) verwendet.

Hier ist ein Beispiel:

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

Ein anderes Beispiel. Wenn Sie den MV * -Ansatz nicht mögen, können Sie etwas anderes verwenden (es gibt einen Schnittpunkt zwischen dem, den ich als nächstes beschreibe, und dem zuletzt genannten). Strukturieren Sie Ihre Anwendung einfach in verschiedene Module. Zum Beispiel bei Twitter.

Twitter Modules

Wenn Sie sich die Benutzeroberfläche ansehen, haben Sie einfach verschiedene Kästchen. Sie können sich jede Box als ein anderes Modul vorstellen. Zum Beispiel können Sie einen Tweet posten. Diese Aktion erfordert die Aktualisierung einiger Module. Zuerst müssen Ihre Profildaten aktualisiert werden (oberes linkes Feld), aber auch Ihre Zeitleiste. Natürlich können Sie Verweise auf beide Module behalten und diese über die öffentliche Schnittstelle separat aktualisieren. Es ist jedoch einfacher (und besser), nur ein Ereignis zu veröffentlichen. Dies erleichtert die Änderung Ihrer Anwendung aufgrund einer lockeren Kopplung. Wenn Sie ein neues Modul entwickeln, das von neuen Tweets abhängt, können Sie einfach das Ereignis "Tweet veröffentlichen" abonnieren und damit umgehen. Dieser Ansatz ist sehr nützlich und kann Ihre Anwendung stark entkoppeln. Sie können Ihre Module sehr einfach wiederverwenden.

Hier ist ein einfaches Beispiel für den letzten Ansatz (dies ist kein Original-Twitter-Code, sondern nur ein Beispiel von mir):

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

Für diesen Ansatz gibt es einen ausgezeichneten Vortrag von Nicholas Zakas . Für den MV * -Ansatz werden die besten mir bekannten Artikel und Bücher von Addy Osmani veröffentlicht.

Nachteile: Sie müssen vorsichtig mit dem übermäßigen Gebrauch von Publish/Subscribe umgehen. Wenn Sie Hunderte von Ereignissen haben, kann es sehr verwirrend werden, alle zu verwalten. Es kann auch zu Kollisionen kommen, wenn Sie den Namespace nicht (oder nicht ordnungsgemäß) verwenden. Eine erweiterte Implementierung von Mediator, die einem Publish/Subscribe ähnelt, finden Sie hier https://github.com/ajacksified/Mediator.js . Es verfügt über Namespaces und Funktionen wie das "Bubbling" von Ereignissen, die natürlich unterbrochen werden können. Ein weiterer Nachteil von Publish/Subscribe ist der harte Unit-Test. Es kann schwierig werden, die verschiedenen Funktionen in den Modulen zu isolieren und unabhängig voneinander zu testen.

218
Minko Gechev

Das Hauptziel besteht darin, die Kopplung zwischen dem Code zu verringern. Es ist eine etwas ereignisbasierte Denkweise, aber die "Ereignisse" sind nicht an ein bestimmtes Objekt gebunden.

Ich schreibe unten ein großes Beispiel in einem Pseudocode, der ein bisschen wie JavaScript aussieht.

Nehmen wir an, wir haben ein Klassenradio und ein Klassenrelais:

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

Immer wenn ein Funkgerät ein Signal empfängt, möchten wir, dass eine Reihe von Relais die Nachricht auf irgendeine Weise weiterleitet. Die Anzahl und Art der Relais kann unterschiedlich sein. Wir könnten es so machen:

class Radio {
    var relayList = [];

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

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

}

Das funktioniert gut. Stellen Sie sich nun vor, wir möchten, dass eine andere Komponente auch an den Signalen der Radio-Klasse teilnimmt, nämlich die Lautsprecher:

(Entschuldigung, wenn die Analogien nicht erstklassig sind ...)

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

Wir könnten das Muster noch einmal wiederholen:

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

    }

}

Wir könnten dies noch verbessern, indem wir eine Schnittstelle wie "SignalListener" erstellen, sodass wir nur eine Liste in der Radio-Klasse benötigen und immer dieselbe Funktion für jedes Objekt aufrufen können, das das Signal hören möchte. Aber das schafft immer noch eine Kopplung zwischen der Schnittstelle/Basisklasse/usw., für die wir uns entscheiden, und der Funkklasse. Grundsätzlich müssen Sie bei jedem Wechsel einer der Klassen Radio, Signal oder Relay überlegen, wie sich dies möglicherweise auf die beiden anderen Klassen auswirkt.

Probieren wir jetzt etwas anderes aus. Erstellen wir eine vierte Klasse namens 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);
        }
    }
}

Jetzt haben wir ein Muster, das uns bekannt ist, und wir können es für eine beliebige Anzahl und Art von Klassen verwenden, solange sie:

  • kennen den RadioMast (die Klasse, die die gesamte Nachrichtenübermittlung abwickelt)
  • kennen die Methodensignatur zum Senden/Empfangen von Nachrichten

Also ändern wir die Radio-Klasse in ihre endgültige, einfache Form:

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

Und wir fügen die Lautsprecher und das Relais der Empfängerliste des RadioMast für diese Art von Signal hinzu:

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

Jetzt hat die Speakers and Relay-Klasse keinerlei Kenntnis mehr, außer dass sie eine Methode hat, die ein Signal empfangen kann, und die Radio-Klasse als Herausgeber kennt den RadioMast, für den sie Signale veröffentlicht. Dies ist der Punkt, an dem ein Nachrichtenübermittlungssystem wie Publish/Subscribe verwendet wird.

15
Anders Arpi

Die anderen Antworten haben großartige Arbeit geleistet und gezeigt, wie das Muster funktioniert. Ich wollte die implizite Frage " was ist los mit dem alten Weg? " ansprechen, während ich gearbeitet habe Mit diesem Muster habe ich vor kurzem zu tun, und ich finde, dass es eine Veränderung in meinem Denken bedeutet.

Stellen Sie sich vor, wir haben ein wirtschaftliches Bulletin abonniert. Das Bulletin veröffentlicht eine Überschrift: " Senken Sie den Dow Jones um 200 Punkte ". Das wäre eine merkwürdige und etwas verantwortungslose Nachricht. Wenn es jedoch veröffentlichte: " Enron hat heute Morgen Insolvenzschutz beantragt ", dann ist dies eine nützlichere Nachricht. Beachten Sie, dass die Nachricht dazu führen kann, dass der Dow Jones um 200 Punkte fällt, aber das ist eine andere Sache.

Es gibt einen Unterschied zwischen dem Senden eines Befehls und dem Benachrichtigen über etwas, das gerade passiert ist. Nehmen Sie in diesem Sinne Ihre ursprüngliche Version des Pub/Sub-Musters und ignorieren Sie den Handler vorerst:

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

Hier besteht bereits eine implizite starke Kopplung zwischen der Benutzeraktion (ein Klick) und der Systemreaktion (eine Bestellung wird entfernt). In Ihrem Beispiel gibt die Aktion effektiv einen Befehl aus. Betrachten Sie diese 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'));
});

Jetzt reagiert der Bearbeiter auf etwas Interessantes, ist jedoch nicht verpflichtet, eine Bestellung zu entfernen. Tatsächlich kann der Handler alle möglichen Dinge tun, die nicht direkt mit dem Entfernen eines Auftrags zusammenhängen, aber dennoch möglicherweise für die aufrufende Aktion relevant sind. Beispielsweise:

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

Die Unterscheidung zwischen einem Befehl und einer Benachrichtigung ist eine nützliche Unterscheidung bei diesem Muster, IMO.

5
Trevedhek

Damit Sie keine Methoden-/Funktionsaufrufe fest codieren müssen, veröffentlichen Sie das Ereignis nur, ohne sich darum zu kümmern, wer zuhört. Dies macht den Publisher unabhängig vom Abonnenten und verringert die Abhängigkeit (oder Kopplung, welcher Begriff auch immer Sie bevorzugen) zwischen zwei verschiedenen Teilen der Anwendung.

Hier sind einige Nachteile der Kopplung, wie von wikipedia erwähnt

Eng gekoppelte Systeme neigen dazu, die folgenden Entwicklungseigenschaften aufzuweisen, die häufig als Nachteile angesehen werden:

  1. Eine Änderung in einem Modul erzwingt normalerweise eine Welligkeit der Änderungen in anderen Modulen.
  2. Der Zusammenbau von Modulen kann aufgrund der erhöhten Abhängigkeit zwischen Modulen mehr Aufwand und/oder Zeit erfordern.
  3. Ein bestimmtes Modul ist möglicherweise schwerer wiederzuverwenden und/oder zu testen, da abhängige Module enthalten sein müssen.

Betrachten Sie so etwas wie ein Objekt, das Geschäftsdaten einkapselt. Der Methodenaufruf zum Aktualisieren der Seite ist fest programmiert, wenn das Alter festgelegt ist:

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

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

//Different module

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

Jetzt kann ich das Personenobjekt nicht testen, ohne auch die Funktion showAge einzuschließen. Wenn ich das Alter auch in einem anderen GUI-Modul anzeigen möchte, muss ich diesen Methodenaufruf in .setAge Fest codieren, und jetzt gibt es Abhängigkeiten für 2 nicht verwandte Module im Personenobjekt. Es ist auch schwierig zu warten, wenn Sie sehen, dass diese Anrufe getätigt werden und sich nicht einmal in derselben Datei befinden.

Beachten Sie, dass Sie innerhalb desselben Moduls natürlich auch direkte Methodenaufrufe haben können. Geschäftsdaten und oberflächliches GUI-Verhalten sollten sich jedoch nach vernünftigen Maßstäben nicht im selben Modul befinden.

4
Esailija

Die Zeitung "Die vielen Gesichter des Publizierens/Abonnierens" ist eine gute Lektüre und eine Sache, die sie hervorheben, ist die Entflechtung in drei "Dimensionen". Hier ist meine grobe Zusammenfassung, aber bitte sehen Sie stattdessen das Papier.

  1. Raum beunruhigend. Die interagierenden Parteien müssen sich nicht kennen. Der Verlag weiß nicht, wer zuhört, wie viele oder was er mit der Veranstaltung macht. Die Abonnenten wissen nicht, wer diese Events produziert oder wie viele Produzenten es usw. gibt.
  2. Zeitliche Entstörung Die interagierenden Parteien müssen während der Interaktion nicht gleichzeitig aktiv sein. Z.B. Ein Abonnent wird möglicherweise getrennt, während ein Publisher einige Ereignisse veröffentlicht. Er kann darauf reagieren, wenn er online ist.
  3. Entkopplung der Synchronisation. Verlage werden beim Erzeugen von Ereignissen nicht blockiert, und Abonnenten können durch Rückrufe asynchron benachrichtigt werden, wenn ein von ihnen abonniertes Ereignis eintrifft.
0
Peheje

Die PubSub-Implementierung wird häufig dort gesehen, wo Folgendes vorhanden ist:

  1. Es gibt eine portletähnliche Implementierung, bei der mehrere Portlets mit Hilfe eines Ereignisbusses kommunizieren. Dies hilft beim Erstellen einer synchronen Architektur.
  2. In einem System mit enger Kopplung ist pubsub ein Mechanismus, der die Kommunikation zwischen verschiedenen Modulen unterstützt.

Beispielcode -

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

Einfache Antwort Die ursprüngliche Frage suchte nach einer einfachen Antwort. Hier ist mein Versuch.

Javascript bietet keinen Mechanismus, mit dem Codeobjekte ihre eigenen Ereignisse erstellen können. Sie brauchen also eine Art Ereignismechanismus. Das Publish/Subscribe-Muster wird diese Anforderung erfüllen, und es liegt an Ihnen, einen Mechanismus zu wählen, der Ihren eigenen Anforderungen am besten entspricht.

Jetzt können wir eine Notwendigkeit für das Pub/Sub-Muster erkennen. Müssen Sie DOM-Ereignisse dann lieber anders behandeln als Ihre Pub/Sub-Ereignisse? Um die Komplexität zu reduzieren und andere Konzepte wie die Trennung von Interessen (SoC) zu nutzen, können Sie den Vorteil sehen, dass alles einheitlich ist.

Paradoxerweise führt mehr Code zu einer besseren Trennung von Anliegen, die sich gut auf sehr komplexe Webseiten skalieren lassen.

Ich hoffe, jemand findet das eine gute Diskussion, ohne ins Detail zu gehen.

0
Simon Miller