Por que alguém usaria o padrão Publicar / Assinar (em JS / jQuery)?


103

Então, um colega me apresentou ao padrão publicar / assinar (em JS / jQuery), mas estou tendo dificuldade em entender por que alguém usaria esse padrão em vez de JavaScript / jQuery 'normal'.

Por exemplo, anteriormente eu tinha o seguinte código ...

$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 eu pude ver o mérito de fazer isso, por exemplo ...

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

Porque introduz a capacidade de reutilizar a removeOrderfuncionalidade para diferentes eventos, etc.

Mas por que você decidiria implementar o padrão publicar / assinar e ir para as seguintes extensões, se ele faz a mesma coisa? (Para sua informação, usei 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'));
});

Eu li sobre o padrão com certeza, mas simplesmente não consigo imaginar por que isso seria necessário. Os tutoriais que vi que explicam como implementar esse padrão cobrem apenas exemplos tão básicos quanto o meu.

Imagino que a utilidade do pub / sub se tornaria aparente em uma aplicação mais complexa, mas não consigo imaginar uma. Receio estar perdendo completamente o ponto; mas gostaria de saber qual é o ponto, se houver!

Você poderia explicar sucintamente por que e em que situações esse padrão é vantajoso? Vale a pena usar o padrão pub / sub para trechos de código como meus exemplos acima?

Respostas:


222

É tudo uma questão de acoplamento fraco e responsabilidade única, o que anda de mãos dadas com os padrões MV * (MVC / MVP / MVVM) em JavaScript que são muito modernos nos últimos anos.

O acoplamento fraco é um princípio orientado a objetos no qual cada componente do sistema conhece sua responsabilidade e não se preocupa com os outros componentes (ou pelo menos tenta não se importar com eles o máximo possível). O acoplamento fraco é uma coisa boa porque você pode reutilizar facilmente os diferentes módulos. Você não está acoplado às interfaces de outros módulos. Usando publicar / assinar, você só estará conectado à interface de publicar / assinar, o que não é um grande problema - apenas dois métodos. Então, se você decidir reutilizar um módulo em um projeto diferente, você pode apenas copiar e colar e provavelmente funcionará ou pelo menos você não precisará de muito esforço para fazê-lo funcionar.

Ao falar sobre acoplamento fraco, devemos mencionar a separação de interesses. Se você estiver construindo um aplicativo usando um padrão de arquitetura MV *, você sempre terá um Modelo (s) e uma Visão (ões). O modelo é a parte comercial do aplicativo. Você pode reutilizá-lo em diferentes aplicativos, portanto, não é uma boa ideia combiná-lo com a Visualização de um único aplicativo, onde deseja mostrá-lo, porque geralmente nos diferentes aplicativos você tem visualizações diferentes. Portanto, é uma boa ideia usar publicar / assinar para a comunicação Model-View. Quando seu Model muda, ele publica um evento, a View o captura e se atualiza. Você não tem nenhuma sobrecarga de publicação / assinatura, isso ajuda você no desacoplamento. Da mesma maneira, você pode manter a lógica da sua aplicação no Controlador, por exemplo (MVVM, MVP não é exatamente um Controlador) e manter a Visualização o mais simples possível. Quando sua View muda (ou o usuário clica em algo, por exemplo), ela apenas publica um novo evento, o Controller o pega e decide o que fazer. Se você estiver familiarizado com oPadrão MVC ou com MVVM em tecnologias Microsoft (WPF / Silverlight), você pode pensar em publicar / assinar como o padrão Observer . Essa abordagem é usada em estruturas como Backbone.js, Knockout.js (MVVM).

Aqui está um exemplo:

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

Outro exemplo. Se você não gosta da abordagem MV *, pode usar algo um pouco diferente (há uma interseção entre o que vou descrever a seguir e o último mencionado). Basta estruturar sua aplicação em diferentes módulos. Por exemplo, veja o Twitter.

Módulos do Twitter

Se você olhar para a interface, simplesmente terá caixas diferentes. Você pode pensar em cada caixa como um módulo diferente. Por exemplo, você pode postar um tweet. Esta ação requer a atualização de alguns módulos. Em primeiro lugar, ele deve atualizar seus dados de perfil (caixa superior esquerda), mas também deve atualizar sua linha do tempo. Claro, você pode manter referências a ambos os módulos e atualizá-los separadamente usando sua interface pública, mas é mais fácil (e melhor) apenas publicar um evento. Isso tornará a modificação de seu aplicativo mais fácil devido ao acoplamento mais fraco. Se você desenvolver um novo módulo que dependa de novos tweets, você pode apenas se inscrever no evento “publicar-tweet” e lidar com isso. Essa abordagem é muito útil e pode tornar seu aplicativo muito desacoplado. Você pode reutilizar seus módulos com muita facilidade.

Aqui está um exemplo básico da última abordagem (este não é o código original do Twitter, é apenas uma amostra minha):

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

Para essa abordagem, há uma excelente palestra de Nicholas Zakas . Para a abordagem MV *, os melhores artigos e livros que conheço são publicados por Addy Osmani .

Desvantagens: você deve ter cuidado com o uso excessivo de publicar / assinar. Se você tem centenas de eventos, pode ficar muito confuso gerenciar todos eles. Você também pode ter colisões se não estiver usando namespacing (ou não da maneira certa). Uma implementação avançada do Mediator que se parece muito com uma publicação / assinatura pode ser encontrada aqui https://github.com/ajacksified/Mediator.js . Ele tem namespacing e recursos como “bubbling” de eventos que, é claro, podem ser interrompidos. Outra desvantagem de publicar / assinar é o teste de unidade rígido, pode ser difícil isolar as diferentes funções nos módulos e testá-los independentemente.


3
Obrigado, isso faz sentido. Estou familiarizado com o padrão MVC, visto que o uso o tempo todo com PHP, mas não tinha pensado nisso em termos de programação orientada a eventos. :)
Maccath de

2
Obrigado por esta descrição. Realmente me ajudou a entender o conceito.
flybear

1
Essa é uma excelente resposta. Não consegui parar de votar nisso :)
Naveed Butt

1
Ótima explicação, vários exemplos, outras sugestões de leitura. A ++.
Carson

16

O principal objetivo é reduzir o acoplamento entre o código. É uma forma de pensar um tanto baseada em eventos, mas os "eventos" não estão ligados a um objeto específico.

Vou escrever um grande exemplo abaixo em algum pseudocódigo que se parece um pouco com JavaScript.

Digamos que temos uma classe Radio e uma 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?
    }
}

Sempre que o rádio recebe um sinal, queremos vários relés para retransmitir a mensagem de alguma forma. O número e os tipos de relés podem ser diferentes. Podemos fazer assim:

class Radio {
    var relayList = [];

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

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

}

Isso funciona bem. Mas agora imagine que queremos um componente diferente para também fazer parte dos sinais que a classe Rádio recebe, a saber: Alto-falantes:

(desculpe se as analogias não são excelentes ...)

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

Poderíamos repetir o padrão novamente:

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

    }

}

Poderíamos tornar isso ainda melhor criando uma interface, como "SignalListener", de modo que só precisemos de uma lista na classe Radio e sempre possamos chamar a mesma função em qualquer objeto que tenhamos que deseja ouvir o sinal. Mas isso ainda cria um acoplamento entre qualquer interface / classe base / etc que decidirmos e a classe Radio. Basicamente, sempre que você muda uma das classes Radio, Signal ou Relay, você tem que pensar sobre como isso poderia afetar as outras duas classes.

Agora vamos tentar algo diferente. Vamos criar uma quarta classe chamada 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);
        }
    }
}

Agora temos um padrão que conhecemos e podemos usá-lo para qualquer número e tipo de classes, desde que:

  • estão cientes do RadioMast (a classe que lida com toda a passagem de mensagens)
  • estão cientes da assinatura do método para enviar / receber mensagens

Portanto, mudamos a classe Radio para sua forma simples e final:

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

E adicionamos os alto-falantes e o relé à lista de receptores do RadioMast para este tipo de sinal:

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

Agora, a classe Speakers and Relay tem conhecimento zero de qualquer coisa, exceto que eles têm um método que pode receber um sinal, e a classe Radio, sendo a editora, está ciente do RadioMast para o qual publica sinais. Este é o ponto de usar um sistema de passagem de mensagens como publicar / assinar.


É muito bom ter um exemplo concreto que mostra como implementar o padrão pub / sub pode ser melhor do que usar métodos 'normais'! Obrigado!
Maccath de

1
De nada! Pessoalmente, muitas vezes acho que meu cérebro não 'clica' quando se trata de novos padrões / metodologias até que percebo um problema real que ele resolve para mim. O padrão de sub / pub é ótimo com arquiteturas que são fortemente acopladas conceitualmente, mas ainda queremos mantê-las separadas o máximo possível. Imagine um jogo onde você tem centenas de objetos que precisam reagir às coisas que acontecem ao seu redor, por exemplo, e esses objetos podem ser tudo: jogador, bala, árvore, geometria, interface gráfica etc. etc.
Anders Arpi

3
JavaScript não tem a classpalavra - chave. Por favor, enfatize este fato, por exemplo. classificando seu código como pseudo-código.
Rob W

Na verdade, no ES6 há uma palavra-chave de classe.
Minko Gechev

5

As outras respostas fizeram um ótimo trabalho ao mostrar como o padrão funciona. Eu queria abordar a questão implícita "o que há de errado com o jeito antigo? ", Pois tenho trabalhado com esse padrão recentemente e acho que envolve uma mudança em meu pensamento.

Imagine que assinamos um boletim econômico. O boletim publica uma manchete: " Baixe o Dow Jones em 200 pontos ". Seria uma mensagem estranha e um tanto irresponsável de enviar. Se, no entanto, publicou: "A Enron entrou com pedido de proteção contra falência, capítulo 11, esta manhã ", então esta é uma mensagem mais útil. Observe que a mensagem pode fazer com que o Dow Jones caia 200 pontos, mas isso é outro assunto.

Há uma diferença entre enviar um comando e avisar sobre algo que acabou de acontecer. Com isso em mente, pegue sua versão original do padrão pub / sub, ignorando o manipulador por enquanto:

$.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á existe um forte acoplamento implícito aqui, entre a ação do usuário (um clique) e a resposta do sistema (um pedido sendo removido). Efetivamente em seu exemplo, a ação é dar um comando. Considere esta versão:

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

Agora, o manipulador está respondendo a algo de interesse que aconteceu, mas não tem obrigação de remover um pedido. Na verdade, o manipulador pode fazer todo tipo de coisa não diretamente relacionada à remoção de um pedido, mas ainda pode ser relevante para a ação de chamada. Por exemplo:

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

A distinção entre um comando e uma notificação é útil para fazer com este padrão, IMO.


Se suas 2 últimas funções ( remindUserToFloss& increaseProgrammerBrowniePoints) estivessem localizadas em módulos separados, você publicaria 2 eventos, um logo após o outro, handleRemoveOrderRequestou flossModulepublicaria um evento em um browniePointsmódulo quando remindUserToFloss()terminar?
Bryan P

4

Para que você não precise codificar chamadas de método / função, basta publicar o evento sem se importar com quem ouve. Isso torna o editor independente do assinante, reduzindo a dependência (ou acoplamento, qualquer termo que você preferir) entre 2 partes diferentes do aplicativo.

Aqui estão algumas desvantagens do acoplamento, conforme mencionado pela wikipedia

Sistemas fortemente acoplados tendem a exibir as seguintes características de desenvolvimento, que muitas vezes são vistas como desvantagens:

  1. Uma mudança em um módulo geralmente força um efeito cascata de mudanças em outros módulos.
  2. A montagem de módulos pode exigir mais esforço e / ou tempo devido à maior dependência entre os módulos.
  3. Um módulo específico pode ser mais difícil de reutilizar e / ou testar porque os módulos dependentes devem ser incluídos.

Considere algo como um objeto encapsulando dados de negócios. Ele tem uma chamada de método codificado para atualizar a página sempre que a idade é definida:

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

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

//Different module

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

Agora não posso testar o objeto pessoa sem incluir também a showAgefunção. Além disso, se eu precisar mostrar a idade em algum outro módulo de GUI também, preciso codificar essa chamada de método .setAgee agora há dependências para 2 módulos não relacionados no objeto pessoa. Também é difícil manter quando você vê essas chamadas sendo feitas e elas nem mesmo estão no mesmo arquivo.

Observe que, dentro do mesmo módulo, você pode, é claro, ter chamadas de método diretas. Mas os dados de negócios e o comportamento superficial da interface do usuário não devem residir no mesmo módulo por nenhum padrão razoável.


Não entendo o conceito de 'dependência' aqui; onde está a dependência no meu segundo exemplo e onde está faltando no meu terceiro? Não consigo ver nenhuma diferença prática entre meu segundo e terceiro fragmentos - parece apenas adicionar uma nova 'camada' entre a função e o evento sem uma razão real. Provavelmente estou sendo cego, mas acho que preciso de mais dicas. :(
Maccath de

1
Você poderia fornecer um exemplo de caso de uso em que publicar / assinar seria mais apropriado do que apenas fazer uma função que executa a mesma coisa?
Jeffrey Sweeney

@Maccath Simplificando: no terceiro exemplo, você não sabe ou precisa saber que removeOrderexiste, então você não pode depender dele. No segundo exemplo, você tem que saber.
Esailija

Embora eu ainda sinta que há maneiras melhores de fazer o que você descreveu aqui, estou pelo menos convencido de que essa metodologia tem um propósito, especialmente em ambientes com muitos outros desenvolvedores. +1
Jeffrey Sweeney

1
@Esailija - Obrigado, acho que entendo um pouco melhor. Então ... se eu removesse o assinante por completo, não haveria erro nem nada, simplesmente não faria nada? E você diria que isso pode ser útil no caso em que deseja executar uma ação, mas não sabe necessariamente qual função é mais relevante no momento da publicação, mas o assinante pode mudar dependendo de outros fatores?
Maccath de

1

A implementação do PubSub é comumente vista onde há -

  1. Há uma implementação semelhante a um portlet, em que vários portlets se comunicam com a ajuda de um barramento de eventos. Isso ajuda na criação de uma arquitetura aync.
  2. Em um sistema prejudicado por um forte acoplamento, pubsub é um mecanismo que ajuda na comunicação entre vários módulos.

Código de exemplo -

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

O artigo "As Muitas Faces de Publicar / Assinar" é uma boa leitura e uma coisa que eles enfatizam é ​​o desacoplamento em três "dimensões". Aqui está meu resumo bruto, mas por favor, consulte o artigo também.

  1. Desacoplamento do espaço. As partes em interação não precisam se conhecer. A editora não sabe quem está ouvindo, quantos estão ouvindo ou o que estão fazendo com o evento. Os assinantes não sabem quem está produzindo esses eventos, quantos produtores existem, etc.
  2. Desacoplamento de tempo. As partes que interagem não precisam estar ativas ao mesmo tempo durante a interação. Por exemplo, um assinante pode ser desconectado enquanto um editor está publicando alguns eventos, mas pode reagir a isso quando estiver online.
  3. Desacoplamento de sincronização. Os editores não são bloqueados durante a produção de eventos e os assinantes podem ser notificados de forma assíncrona por meio de retornos de chamada sempre que um evento ao qual eles se inscreveram chega.

0

Resposta simples A pergunta original procurava uma resposta simples. Aqui está minha tentativa.

Javascript não fornece nenhum mecanismo para que objetos de código criem seus próprios eventos. Portanto, você precisa de um tipo de mecanismo de evento. o padrão Publicar / assinar atenderá a essa necessidade e cabe a você escolher o mecanismo que melhor atende às suas necessidades.

Agora podemos ver a necessidade do padrão pub / sub, então você prefere lidar com eventos DOM de forma diferente de como você lida com seus eventos pub / sub? Para reduzir a complexidade e outros conceitos, como separação de interesses (SoC), você pode ver o benefício de tudo ser uniforme.

Então, paradoxalmente, mais código cria uma melhor separação de interesses, que pode ser escalada para páginas da web muito complexas.

Espero que alguém considere esta discussão boa o suficiente, sem entrar em detalhes.

Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.