O padrão Publish-Subscribe no Rails: um tutorial de implementação
Publicados: 2022-03-11O padrão de publicação-assinatura (ou pub/sub, abreviado) é um padrão de mensagens Ruby on Rails em que os remetentes de mensagens (editores) não programam as mensagens para serem enviadas diretamente a destinatários específicos (assinantes). Em vez disso, o programador “publica” mensagens (eventos), sem qualquer conhecimento de quaisquer assinantes que possam existir.
Da mesma forma, os assinantes manifestam interesse em um ou mais eventos e recebem apenas mensagens de seu interesse, sem o conhecimento de nenhum editor.
Para isso, um intermediário, chamado de “corretor de mensagens” ou “barramento de eventos”, recebe as mensagens publicadas e as encaminha para os assinantes cadastrados para recebê-las.
Em outras palavras, pub-sub é um padrão usado para comunicar mensagens entre diferentes componentes do sistema sem que esses componentes saibam nada sobre a identidade um do outro.
Este padrão de design não é novo, mas não é comumente usado por desenvolvedores Rails. Existem muitas ferramentas que ajudam a incorporar esse padrão de design em sua base de código, como:
- Wisper (que eu pessoalmente prefiro e vou discutir mais)
- EventBus
- EventBGBus (um fork do EventBus)
- Coelho MQ
- Redis
Todas essas ferramentas têm diferentes implementações de pub-sub subjacentes, mas todas oferecem as mesmas vantagens principais para uma aplicação Rails.
Vantagens da implementação do Pub-Sub
Reduzindo o inchaço do modelo/controlador
É uma prática comum, mas não uma prática recomendada, ter alguns modelos ou controladores gordos em sua aplicação Rails.
O padrão pub/sub pode facilmente ajudar a decompor modelos ou controladores de gordura.
Menos retornos de chamada
Ter muitos retornos de chamada entrelaçados entre os modelos é um cheiro de código bem conhecido e, pouco a pouco, une firmemente os modelos, tornando-os mais difíceis de manter ou estender.
Por exemplo, um modelo Post
pode ter a seguinte aparência:
# app/models/post.rb class Post # ... field: content, type: String # ... after_create :create_feed, :notify_followers # ... def create_feed Feed.create!(self) end def notify_followers User::NotifyFollowers.call(self) end end
E o controlador Post
pode se parecer com o seguinte:
# app/controllers/api/v1/posts_controller.rb class Api::V1::PostsController < Api::V1::ApiController # ... def create @post = current_user.posts.build(post_params) if @post.save render_created(@post) else render_unprocessable_entity(@post.errors) end end # ... end
Como você pode ver, o modelo Post
tem retornos de chamada que acoplam firmemente o modelo ao modelo Feed
e ao serviço ou preocupação User::NotifyFollowers
. Ao usar qualquer padrão pub/sub, o código anterior pode ser refatorado para ser algo como o seguinte, que usa o Wisper:
# app/models/post.rb class Post # ... field: content, type: String # ... # no callbacks in the models! end
Os editores publicam o evento com o objeto de carga útil do evento que pode ser necessário.
# app/controllers/api/v1/posts_controller.rb # corresponds to the publisher in the previous figure class Api::V1::PostsController < Api::V1::ApiController include Wisper::Publisher # ... def create @post = current_user.posts.build(post_params) if @post.save # Publish event about post creation for any interested listeners publish(:post_create, @post) render_created(@post) else # Publish event about post error for any interested listeners publish(:post_errors, @post) render_unprocessable_entity(@post.errors) end end # ... end
Os assinantes assinam apenas os eventos aos quais desejam responder.
# app/listener/feed_listener.rb class FeedListener def post_create(post) Feed.create!(post) end end
# app/listener/user_listener.rb class UserListener def post_create(post) User::NotifyFollowers.call(self) end end
O Event Bus registra os diferentes assinantes no sistema.
# config/initializers/wisper.rb Wisper.subscribe(FeedListener.new) Wisper.subscribe(UserListener.new)
Neste exemplo, o padrão pub-sub eliminou completamente os retornos de chamada no modelo Post
e ajudou os modelos a trabalhar de forma independente uns dos outros com conhecimento mínimo um sobre o outro, garantindo um baixo acoplamento. Expandir o comportamento para ações adicionais é apenas uma questão de se conectar ao evento desejado.
O Princípio da Responsabilidade Única (SRP)
O Princípio da Responsabilidade Única é realmente útil para manter uma base de código limpa. O problema em ater-se a isso é que às vezes a responsabilidade da classe não é tão clara quanto deveria. Isso é especialmente comum quando se trata de MVCs (como Rails).
Os modelos devem lidar com persistência, associações e nada mais.
Os controladores devem lidar com as solicitações do usuário e ser um wrapper em torno da lógica de negócios (objetos de serviço).
Os Objetos de Serviço devem encapsular uma das responsabilidades da lógica de negócios, fornecer um ponto de entrada para serviços externos ou atuar como uma alternativa às preocupações do modelo.
Graças ao seu poder de reduzir o acoplamento, o padrão de design pub-sub pode ser combinado com objetos de serviço de responsabilidade única (SRSOs) para ajudar a encapsular a lógica de negócios e impedir que a lógica de negócios se infiltre nos modelos ou nos controladores. Isso mantém a base de código limpa, legível, sustentável e escalável.
Aqui está um exemplo de alguma lógica de negócios complexa implementada usando o padrão pub/sub e objetos de serviço:
Editor
# app/service/financial/order_review.rb class Financial::OrderReview include Wisper::Publisher # ... def self.call(order) if order.approved? publish(:order_create, order) else publish(:order_decline, order) end end # ...
Assinantes
# app/listener/client_listener.rb class ClientListener def order_create(order) # can implement transaction using different service objects Client::Charge.call(order) Inventory::UpdateStock.call(order) end def order_decline(order) Client::NotifyDeclinedOrder(order) end end
Ao usar o padrão de publicação-assinatura, a base de código é organizada em SRSOs quase automaticamente. Além disso, a implementação de código para fluxos de trabalho complexos é facilmente organizada em torno de eventos, sem sacrificar a legibilidade, a capacidade de manutenção ou a escalabilidade.
Teste
Ao decompor os modelos e controladores gordos e ter muitos SRSOs, o teste da base de código se torna um processo muito, muito mais fácil. Este é particularmente o caso quando se trata de testes de integração e comunicação entre módulos. O teste deve simplesmente garantir que os eventos sejam publicados e recebidos corretamente.

Wisper tem uma gema de teste que adiciona matchers RSpec para facilitar o teste de diferentes componentes.
Nos dois exemplos anteriores (exemplo de Post
e exemplo de Order
), o teste deve incluir o seguinte:
Editores
# spec/service/financial/order_review.rb describe Financial::OrderReview do it 'publishes :order_create' do @order = Fabricate(:order, approved: true) expect { Financial::OrderReview.call(@order) }.to broadcast(:order_create) end it 'publishes :order_decline' do @order = Fabricate(:order, approved: false) expect { Financial::OrderReview.call(@order) }.to broadcast(:order_decline) end end
Assinantes
# spec/listeners/feed_listener_spec.rb describe FeedListener do it 'receives :post_create event on PostController#create' do expect(FeedListner).to receive(:post_create).with(Post.last) post '/post', { content: 'Some post content' }, request_headers end end
No entanto, existem algumas limitações para testar os eventos publicados quando o editor é o controlador.
Se você quiser ir além, testar a carga útil também ajudará a manter uma base de código ainda melhor.
Como você pode ver, o teste de padrão de design pub-sub é simples. Trata-se apenas de garantir que os diferentes eventos sejam publicados e recebidos corretamente.
atuação
Esta é mais uma possível vantagem. O próprio padrão de design de publicação-assinatura não tem um grande impacto inerente no desempenho do código. No entanto, como acontece com qualquer ferramenta que você usa em seu código, as ferramentas para implementar pub/sub podem ter um grande efeito no desempenho. Às vezes pode ser um efeito ruim, mas às vezes pode ser muito bom.
Primeiro, um exemplo de um efeito ruim: o Redis é um “cache e armazenamento de valor-chave avançado. É muitas vezes referido como um servidor de estrutura de dados.” Esta ferramenta popular suporta o padrão pub/sub e é muito estável. No entanto, se for usado em um servidor remoto (não no mesmo servidor em que o aplicativo Rails está implantado), resultará em uma enorme perda de desempenho devido à sobrecarga da rede.
Por outro lado, o Wisper possui vários adaptadores para manipulação de eventos assíncronos, como wisper-celluloid, wisper-sidekiq e wisper-activejob. Essas ferramentas oferecem suporte a eventos assíncronos e execuções encadeadas. que, se aplicado adequadamente, pode aumentar enormemente o desempenho do aplicativo.
A linha inferior
Se você está buscando a milha extra no desempenho, o padrão pub/sub pode ajudá-lo a alcançá-lo. Mas mesmo que você não encontre um aumento de desempenho com esse padrão de design do Rails, ele ainda ajudará a manter o código organizado e torná-lo mais sustentável. Afinal, quem pode se preocupar com o desempenho de um código que não pode ser mantido ou que não funciona?
Desvantagens da implementação do Pub-Sub
Tal como acontece com todas as coisas, também existem algumas desvantagens possíveis para o padrão pub-sub.
Acoplamento solto (acoplamento semântico inflexível)
Os maiores pontos fortes do padrão pub/sub são também suas maiores fraquezas. A estrutura dos dados publicados (a carga útil do evento) deve ser bem definida e rapidamente se torna bastante inflexível. Para modificar a estrutura de dados da carga útil publicada, é necessário conhecer todos os Assinantes e modificá-los também ou garantir que as modificações sejam compatíveis com versões mais antigas. Isso torna a refatoração do código do Publisher muito mais difícil.
Se você quiser evitar isso, deve ser extremamente cauteloso ao definir a carga útil dos editores. Claro, se você tiver um ótimo conjunto de testes, que testa a carga útil, assim como mencionado anteriormente, você não precisa se preocupar muito com a queda do sistema depois de alterar a carga útil do editor ou o nome do evento.
Estabilidade do barramento de mensagens
Os editores não têm conhecimento do status do assinante e vice-versa. Usando ferramentas simples de publicação/assinatura, pode não ser possível garantir a estabilidade do próprio barramento de mensagens e garantir que todas as mensagens publicadas sejam enfileiradas e entregues corretamente.
O número crescente de mensagens trocadas leva a instabilidades no sistema ao usar ferramentas simples, e pode não ser possível garantir a entrega a todos os assinantes sem alguns protocolos mais sofisticados. Dependendo de quantas mensagens estão sendo trocadas e dos parâmetros de desempenho que você deseja alcançar, considere usar serviços como RabbitMQ, PubNub, Pusher, CloudAMQP, IronMQ ou muitas outras alternativas. Essas alternativas fornecem funcionalidade extra e são mais estáveis que o Wisper para sistemas mais complexos. No entanto, eles também exigem algum trabalho extra para implementar. Você pode ler mais sobre como os corretores de mensagens funcionam aqui
Loops de eventos infinitos
Quando o sistema é totalmente controlado por eventos, você deve ser extremamente cauteloso para não ter loops de eventos. Esses loops são como os loops infinitos que podem acontecer no código. No entanto, eles são mais difíceis de detectar com antecedência e podem paralisar seu sistema. Eles podem existir sem seu aviso quando houver muitos eventos publicados e inscritos no sistema.
Conclusão do tutorial Rails
O padrão de publicação-assinatura não é uma bala de prata para todos os seus problemas e cheiros de código do Rails, mas é um padrão de design muito bom que ajuda a desacoplar diferentes componentes do sistema e torná-lo mais sustentável, legível e escalável.
Quando combinado com objetos de serviço de responsabilidade única (SRSOs), o pub-sub também pode realmente ajudar a encapsular a lógica de negócios e impedir que diferentes preocupações de negócios se infiltrem nos modelos ou nos controladores.
O ganho de desempenho após usar esse padrão depende principalmente da ferramenta subjacente que está sendo usada, mas o ganho de desempenho pode ser melhorado significativamente em alguns casos e, na maioria dos casos, certamente não prejudicará o desempenho.
No entanto, o uso do padrão pub-sub deve ser estudado e planejado com cuidado, pois com o grande poder do acoplamento fraco vem a grande responsabilidade de manter e refatorar componentes fracamente acoplados.
Como os eventos podem facilmente ficar fora de controle, uma biblioteca pub/sub simples pode não garantir a estabilidade do agente de mensagens.
E, finalmente, existe o perigo de introduzir loops de eventos infinitos que passam despercebidos até que seja tarde demais.
Estou usando esse padrão há quase um ano e é difícil imaginar escrever código sem ele. Para mim, é a cola que faz com que trabalhos em segundo plano, objetos de serviço, interesses, controladores e modelos se comuniquem entre si de forma limpa e funcionem juntos como charme.
Espero que você tenha aprendido tanto quanto eu ao revisar este código e que você se sinta inspirado a dar ao padrão Publish-Subscribe uma chance de tornar seu aplicativo Rails incrível.
Finalmente, um enorme obrigado a @krisleech por seu trabalho incrível na implementação do Wisper.