Perché si dovrebbe usare il modello di pubblicazione / sottoscrizione (in JS / jQuery)?


103

Quindi, un collega mi ha presentato il modello di pubblicazione / sottoscrizione (in JS / jQuery), ma ho difficoltà a capire perché si dovrebbe utilizzare questo modello su JavaScript / jQuery "normale".

Ad esempio, in precedenza avevo il seguente codice ...

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

E potrei vedere il merito di farlo invece, per esempio ...

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

Perché introduce la possibilità di riutilizzare la removeOrderfunzionalità per diversi eventi, ecc.

Ma perché decidi di implementare il modello di pubblicazione / sottoscrizione e passare alle seguenti lunghezze, se fa la stessa cosa? (Cordiali saluti, ho usato 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'));
});

Ho letto di sicuro lo schema, ma non riesco a immaginare perché questo sarebbe mai stato necessario. I tutorial che ho visto che spiegano come implementare questo modello coprono solo esempi di base come il mio.

Immagino che l'utilità del pub / sub si manifesterebbe in un'applicazione più complessa, ma non riesco a immaginarne una. Ho paura di perdere completamente il punto; ma mi piacerebbe sapere il punto se ce n'è uno!

Potreste spiegare succintamente perché e in quali situazioni questo schema è vantaggioso? Vale la pena utilizzare il modello pub / sub per frammenti di codice come i miei esempi sopra?

Risposte:


222

Si tratta di accoppiamento libero e responsabilità singola, che va di pari passo con i pattern MV * (MVC / MVP / MVVM) in JavaScript che sono molto moderni negli ultimi anni.

L'accoppiamento allentato è un principio orientato agli oggetti in cui ogni componente del sistema conosce la propria responsabilità e non si preoccupa degli altri componenti (o almeno cerca di non curarsene il più possibile). L'accoppiamento lento è una buona cosa perché puoi facilmente riutilizzare i diversi moduli. Non sei accoppiato con le interfacce di altri moduli. Usando la pubblicazione / sottoscrizione sei solo accoppiato con l'interfaccia di pubblicazione / sottoscrizione che non è un grosso problema - solo due metodi. Quindi se decidi di riutilizzare un modulo in un progetto diverso puoi semplicemente copiarlo e incollarlo e probabilmente funzionerà o almeno non avrai bisogno di molto sforzo per farlo funzionare.

Quando si parla di accoppiamento lento, dovremmo menzionare la separazione delle preoccupazioni. Se stai creando un'applicazione utilizzando un modello architettonico MV *, hai sempre uno o più modelli e una o più viste. Il modello è la parte aziendale dell'applicazione. Puoi riutilizzarlo in diverse applicazioni, quindi non è una buona idea accoppiarlo con la Vista di una singola applicazione, dove vuoi mostrarlo, perché di solito nelle diverse applicazioni hai visualizzazioni diverse. Quindi è una buona idea usare la pubblicazione / sottoscrizione per la comunicazione Model-View. Quando il tuo modello cambia, pubblica un evento, la vista lo cattura e si aggiorna. Non hai alcun sovraccarico dalla pubblicazione / sottoscrizione, ti aiuta per il disaccoppiamento. Allo stesso modo puoi mantenere la logica dell'applicazione nel controller, ad esempio (MVVM, MVP non è esattamente un controller) e mantenere la visualizzazione il più semplice possibile. Quando la tua Vista cambia (o l'utente fa clic su qualcosa, per esempio) pubblica solo un nuovo evento, il Controller lo cattura e decide cosa fare. Se hai familiarità con ilPattern MVC o con MVVM nelle tecnologie Microsoft (WPF / Silverlight) puoi pensare alla pubblicazione / sottoscrizione come al pattern Observer . Questo approccio viene utilizzato in framework come Backbone.js, Knockout.js (MVVM).

Ecco un esempio:

//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 altro esempio. Se non ti piace l'approccio MV * puoi usare qualcosa di leggermente diverso (c'è un'intersezione tra quello che descriverò di seguito e l'ultimo menzionato). Basta strutturare la tua applicazione in diversi moduli. Ad esempio guarda Twitter.

Moduli Twitter

Se guardi l'interfaccia hai semplicemente diverse caselle. Puoi pensare a ogni scatola come a un modulo diverso. Ad esempio puoi pubblicare un tweet. Questa azione richiede l'aggiornamento di alcuni moduli. In primo luogo deve aggiornare i dati del tuo profilo (riquadro in alto a sinistra) ma deve anche aggiornare la tua cronologia. Naturalmente, puoi mantenere i riferimenti a entrambi i moduli e aggiornarli separatamente usando la loro interfaccia pubblica, ma è più facile (e migliore) pubblicare solo un evento. Ciò renderà più semplice la modifica dell'applicazione a causa di un accoppiamento più allentato. Se sviluppi un nuovo modulo che dipende dai nuovi tweet, puoi semplicemente iscriverti all'evento "publish-tweet" e gestirlo. Questo approccio è molto utile e può rendere la tua applicazione molto disaccoppiata. Puoi riutilizzare i tuoi moduli molto facilmente.

Ecco un esempio di base dell'ultimo approccio (questo non è il codice Twitter originale, è solo un mio esempio):

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

Per questo approccio c'è un eccellente discorso di Nicholas Zakas . Per l'approccio MV * i migliori articoli e libri che conosco sono pubblicati da Addy Osmani .

Svantaggi: devi stare attento all'uso eccessivo di pubblica / sottoscrizione. Se hai centinaia di eventi può diventare molto complicato gestirli tutti. Potresti anche avere collisioni se non stai usando lo spazio dei nomi (o non lo stai usando nel modo giusto). Un'implementazione avanzata di Mediator che assomiglia molto a una pubblicazione / sottoscrizione può essere trovata qui https://github.com/ajacksified/Mediator.js . Ha spazi dei nomi e funzioni come il "bubbling" di eventi che, ovviamente, può essere interrotto. Un altro svantaggio della pubblicazione / sottoscrizione è l'hard unit testing, potrebbe diventare difficile isolare le diverse funzioni nei moduli e testarle in modo indipendente.


3
Grazie, ha senso. Conosco il pattern MVC poiché lo uso sempre con PHP, ma non ci avevo pensato in termini di programmazione guidata dagli eventi. :)
Maccath

2
Grazie per questa descrizione. Mi ha davvero aiutato a comprendere il concetto.
flybear

1
Questa è un'ottima risposta. Non potevo impedirmi di votare questo :)
Naveed Butt

1
Ottima spiegazione, molteplici esempi, ulteriori suggerimenti di lettura. A ++.
Carson

16

L'obiettivo principale è ridurre l'accoppiamento tra il codice. È un modo di pensare in qualche modo basato sugli eventi, ma gli "eventi" non sono legati a un oggetto specifico.

Scriverò un grande esempio di seguito in uno pseudo codice che assomiglia un po 'a JavaScript.

Supponiamo di avere una classe Radio e una classe Relay:

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

Ogni volta che la radio riceve un segnale, vogliamo che un certo numero di relè trasmetta il messaggio in qualche modo. Il numero e il tipo di relè possono variare. Potremmo farlo in questo modo:

class Radio {
    var relayList = [];

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

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

}

Funziona bene. Ma ora immagina di volere che un componente diverso prenda parte anche dei segnali che riceve la classe Radio, ovvero Altoparlanti:

(scusate se le analogie non sono di prim'ordine ...)

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

Potremmo ripetere di nuovo lo schema:

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

    }

}

Potremmo renderlo ancora migliore creando un'interfaccia, come "SignalListener", in modo che abbiamo solo bisogno di una lista nella classe Radio, e possiamo sempre chiamare la stessa funzione su qualsiasi oggetto che abbiamo che vuole ascoltare il segnale. Ma questo crea comunque un accoppiamento tra qualunque interfaccia / classe base / ecc. Decidiamo su e la classe Radio. Fondamentalmente ogni volta che cambi una delle classi Radio, Segnale o Relè devi pensare a come potrebbe influenzare le altre due classi.

Ora proviamo qualcosa di diverso. Creiamo una quarta classe denominata 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);
        }
    }
}

Ora abbiamo un modello di cui siamo consapevoli e possiamo usarlo per qualsiasi numero e tipo di classi purché:

  • sono a conoscenza del RadioMast (la classe che gestisce tutto il passaggio di messaggi)
  • sono a conoscenza della firma del metodo per inviare / ricevere messaggi

Quindi cambiamo la classe Radio nella sua forma finale e semplice:

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

E aggiungiamo gli altoparlanti e il relè all'elenco dei ricevitori del RadioMast per questo tipo di segnale:

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

Ora la classe Speakers and Relay non ha alcuna conoscenza di nulla tranne che ha un metodo che può ricevere un segnale, e la classe Radio, essendo l'editore, è a conoscenza del RadioMast a cui pubblica i segnali. Questo è il punto di utilizzare un sistema di passaggio di messaggi come pubblica / sottoscrizione.


Davvero fantastico avere un esempio concreto che mostra come l'implementazione del pattern pub / sub possa essere migliore rispetto all'utilizzo di metodi "normali"! Grazie!
Maccath

1
Prego! Personalmente trovo spesso che il mio cervello non "scatta" quando si tratta di nuovi schemi / metodologie fino a quando non mi rendo conto di un problema reale che risolve per me. Il modello sub / pub è ottimo con architetture strettamente accoppiate concettualmente, ma vogliamo comunque tenerle separate il più possibile. Immagina un gioco in cui hai centinaia di oggetti che devono reagire a ciò che accade intorno a loro, ad esempio, e questi oggetti possono essere qualsiasi cosa: giocatore, proiettile, albero, geometria, gui ecc. Ecc.
Anders Arpi

3
JavaScript non ha la classparola chiave. Si prega di sottolineare questo fatto, ad es. classificando il codice come pseudo-codice.
Rob W

In realtà in ES6 c'è una parola chiave class.
Minko Gechev

5

Le altre risposte hanno fatto un ottimo lavoro nel mostrare come funziona lo schema. Volevo affrontare la domanda implicita " cosa c'è di sbagliato nel vecchio modo? " Poiché ho lavorato di recente con questo schema e trovo che implichi un cambiamento nel mio pensiero.

Immagina di esserci abbonati a un bollettino economico. Il bollettino pubblica un titolo: " Abbassare il Dow Jones di 200 punti ". Sarebbe un messaggio strano e un po 'irresponsabile da inviare. Se, tuttavia, ha pubblicato: "La Enron ha presentato istanza di protezione dal fallimento del capitolo 11 questa mattina ", allora questo è un messaggio più utile. Notare che il messaggio può far scendere il Dow Jones di 200 punti, ma questa è un'altra questione.

C'è una differenza tra inviare un comando e avvisare di qualcosa che è appena accaduto. Con questo in mente, prendi la tua versione originale del pattern pub / sub, ignorando per ora il gestore:

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

C'è già un forte accoppiamento implicito qui, tra l'azione dell'utente (un clic) e la risposta del sistema (un ordine rimosso). In modo efficace nel tuo esempio, l'azione è dare un comando. Considera questa versione:

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

Ora il conduttore sta rispondendo a qualcosa di interessante che è accaduto, ma non ha l'obbligo di rimuovere un ordine. In effetti, il gestore può fare ogni sorta di cose non direttamente correlate alla rimozione di un ordine, ma forse comunque rilevanti per l'azione chiamante. Per esempio:

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 distinzione tra un comando e una notifica è un'utile distinzione da fare con questo modello, IMO.


se le tue ultime 2 funzioni ( remindUserToFloss& increaseProgrammerBrowniePoints) si trovassero in moduli separati, pubblicheresti 2 eventi uno dopo l'altro proprio lì dentro handleRemoveOrderRequesto faresti flossModulepubblicare un evento su un browniePointsmodulo quando remindUserToFloss()è finito?
Bryan P

4

In modo che non sia necessario codificare le chiamate di metodi / funzioni, è sufficiente pubblicare l'evento senza preoccuparsi di chi ascolta. Ciò rende l'editore indipendente dall'abbonato, riducendo la dipendenza (o l'accoppiamento, qualunque termine si preferisca) tra 2 diverse parti dell'applicazione.

Ecco alcuni svantaggi dell'accoppiamento come menzionato da wikipedia

I sistemi strettamente accoppiati tendono a mostrare le seguenti caratteristiche di sviluppo, che sono spesso viste come svantaggi:

  1. Un cambiamento in un modulo di solito forza un effetto a catena dei cambiamenti in altri moduli.
  2. L'assemblaggio dei moduli potrebbe richiedere più impegno e / o tempo a causa della maggiore dipendenza tra i moduli.
  3. Un particolare modulo potrebbe essere più difficile da riutilizzare e / o testare perché i moduli dipendenti devono essere inclusi.

Considera qualcosa come un oggetto che incapsula i dati aziendali. Ha una chiamata al metodo hard coded per aggiornare la pagina ogni volta che viene impostata l'età:

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

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

//Different module

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

Ora non posso testare l'oggetto persona senza includere anche la showAgefunzione. Inoltre, se ho bisogno di mostrare l'età anche in qualche altro modulo della GUI, ho bisogno di codificare tale chiamata di metodo .setAge, e ora ci sono dipendenze per 2 moduli non correlati nell'oggetto persona. È anche difficile da mantenere quando vedi quelle chiamate fatte e non sono nemmeno nello stesso file.

Nota che all'interno dello stesso modulo, puoi ovviamente avere chiamate dirette al metodo. Ma i dati aziendali e il comportamento superficiale della GUI non dovrebbero risiedere nello stesso modulo secondo standard ragionevoli.


Non capisco il concetto di "dipendenza" qui; dov'è la dipendenza nel secondo esempio e dove manca nel terzo? Non riesco a vedere alcuna differenza pratica tra il mio secondo e terzo frammento: sembra semplicemente aggiungere un nuovo "livello" tra la funzione e l'evento senza una vera ragione. Probabilmente sono cieco, ma penso di aver bisogno di più indicazioni. :(
Maccath

1
Potreste fornire un caso d'uso di esempio in cui la pubblicazione / sottoscrizione sarebbe più appropriata della semplice creazione di una funzione che esegue la stessa cosa?
Jeffrey Sweeney

@Maccath In poche parole: nel terzo esempio, non sai o devi sapere che removeOrderesiste, quindi non puoi dipendere da esso. Nel secondo esempio, devi sapere.
Esailija

Anche se sento ancora che ci sono modi migliori per affrontare ciò che hai descritto qui, sono almeno convinto che questa metodologia abbia uno scopo, specialmente in ambienti con molti altri sviluppatori. +1
Jeffrey Sweeney

1
@Esailija - Grazie, penso di aver capito un po 'meglio. Quindi ... se rimuovessi completamente l'abbonato, non si verificherebbe alcun errore o altro, semplicemente non farebbe nulla? E diresti che questo potrebbe essere utile nel caso in cui desideri eseguire un'azione, ma non sapresti necessariamente quale funzione è più rilevante al momento della pubblicazione, ma l'abbonato potrebbe cambiare in base ad altri fattori?
Maccath

1

L'implementazione di PubSub è comunemente vista dove c'è -

  1. Esiste un'implementazione simile a un portlet in cui sono presenti più portlet che comunicano con l'aiuto di un bus di eventi. Questo aiuta nella creazione di un'architettura sincronizzata.
  2. In un sistema rovinato da un accoppiamento stretto, pubsub è un meccanismo che aiuta a comunicare tra i vari moduli.

Codice di esempio -

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

Il documento "The Many Faces of Publish / Subscribe" è una buona lettura e una cosa che sottolineano è il disaccoppiamento in tre "dimensioni". Ecco il mio sommario grezzo, ma per favore fai riferimento anche al documento.

  1. Disaccoppiamento spaziale. Le parti che interagiscono non hanno bisogno di conoscersi. L'editore non sa chi sta ascoltando, quanti stanno ascoltando o cosa stanno facendo con l'evento. Gli abbonati non sanno chi sta producendo questi eventi, quanti produttori ci sono, ecc.
  2. Disaccoppiamento temporale. Non è necessario che le parti interagenti siano attive contemporaneamente durante l'interazione. Ad esempio, un abbonato potrebbe essere disconnesso mentre un editore pubblica alcuni eventi, ma può reagire quando torna online.
  3. Disaccoppiamento della sincronizzazione. Gli editori non vengono bloccati durante la produzione di eventi e gli abbonati possono essere informati in modo asincrono tramite callback ogni volta che arriva un evento a cui si sono iscritti.

0

Risposta semplice La domanda originale cercava una risposta semplice. Ecco il mio tentativo.

Javascript non fornisce alcun meccanismo per gli oggetti codice per creare i propri eventi. Quindi hai bisogno di una sorta di meccanismo degli eventi. il modello di pubblicazione / sottoscrizione risponderà a questa esigenza e sta a te scegliere un meccanismo che meglio si adatta alle tue esigenze.

Ora possiamo vedere la necessità del pattern pub / sub, quindi preferiresti gestire gli eventi DOM in modo diverso da come gestisci i tuoi eventi pub / sub? Per ridurre la complessità e altri concetti come la separazione delle preoccupazioni (SoC), potresti vedere il vantaggio che tutto è uniforme.

Quindi, paradossalmente, più codice crea una migliore separazione delle preoccupazioni, che si adatta bene a pagine web molto complesse.

Spero che qualcuno trovi questa discussione abbastanza buona senza entrare nei dettagli.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.