Il modello Publish-Subscribe on Rails: un tutorial sull'implementazione
Pubblicato: 2022-03-11Il modello publish-subscribe (o pub/sub, in breve) è un modello di messaggistica Ruby on Rails in cui i mittenti di messaggi (editori), non programmano i messaggi da inviare direttamente a destinatari specifici (abbonati). Il programmatore, invece, “pubblica” messaggi (eventi), all'insaputa di eventuali abbonati.
Allo stesso modo, gli abbonati esprimono interesse per uno o più eventi e ricevono solo messaggi di interesse, all'insaputa degli editori.
A tal fine, un intermediario, chiamato "mediatore di messaggi" o "bus di eventi", riceve i messaggi pubblicati e quindi li inoltra agli abbonati che sono registrati per riceverli.
In altre parole, pub-sub è un modello utilizzato per comunicare messaggi tra diversi componenti del sistema senza che questi componenti sappiano nulla dell'identità dell'altro.
Questo modello di progettazione non è nuovo, ma non è comunemente utilizzato dagli sviluppatori di Rails. Ci sono molti strumenti che aiutano a incorporare questo modello di progettazione nella tua base di codice, come ad esempio:
- Wisper (che personalmente preferisco e ne parlerò ulteriormente)
- EventBus
- EventBGBus (un fork di EventBus)
- Coniglio MQ
- Redis
Tutti questi strumenti hanno diverse implementazioni pub-sub sottostanti, ma offrono tutti gli stessi vantaggi principali per un'applicazione Rails.
Vantaggi dell'implementazione Pub-Sub
Ridurre il rigonfiamento del modello/controllore
È una pratica comune, ma non una best practice, avere alcuni modelli o controller fat nell'applicazione Rails.
Il modello pub/sub può facilmente aiutare a scomporre modelli o controller di grasso.
Meno richiamate
Avere molti callback intrecciati tra i modelli è un noto odore di codice e, a poco a poco, accoppia strettamente i modelli insieme, rendendoli più difficili da mantenere o estendere.
Ad esempio, un modello Post
potrebbe essere simile al seguente:
# 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 il controller Post
potrebbe assomigliare al seguente:
# 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
Come puoi vedere, il modello Post
ha callback che accoppiano strettamente il modello sia al modello Feed
che al servizio o preoccupazione User::NotifyFollowers
. Utilizzando qualsiasi modello pub/sub, il codice precedente potrebbe essere rifattorizzato per essere qualcosa di simile al seguente, che utilizza Wisper:
# app/models/post.rb class Post # ... field: content, type: String # ... # no callbacks in the models! end
Gli editori pubblicano l'evento con l'oggetto payload dell'evento che potrebbe essere necessario.
# 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
Gli abbonati si iscrivono solo agli eventi a cui desiderano rispondere.
# 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
Event Bus registra i diversi abbonati nel sistema.
# config/initializers/wisper.rb Wisper.subscribe(FeedListener.new) Wisper.subscribe(UserListener.new)
In questo esempio, il modello pub-sub ha eliminato completamente i callback nel modello Post
e ha aiutato i modelli a lavorare indipendentemente l'uno dall'altro con una conoscenza minima l'uno dell'altro, garantendo un accoppiamento libero. Espandere il comportamento ad azioni aggiuntive è solo questione di agganciarsi all'evento desiderato.
Il principio di responsabilità unica (SRP)
Il principio della responsabilità unica è davvero utile per mantenere una base di codice pulita. Il problema nell'attenersi è che a volte la responsabilità della classe non è chiara come dovrebbe essere. Questo è particolarmente comune quando si tratta di MVC (come Rails).
I modelli dovrebbero gestire la persistenza, le associazioni e non molto altro.
I controller devono gestire le richieste degli utenti ed essere un wrapper attorno alla logica aziendale (oggetti di servizio).
Gli oggetti di servizio dovrebbero incapsulare una delle responsabilità della logica aziendale, fornire un punto di ingresso per servizi esterni o fungere da alternativa ai problemi del modello.
Grazie al suo potere di ridurre l'accoppiamento, il modello di progettazione pub-sub può essere combinato con oggetti di servizio a responsabilità singola (SRSO) per aiutare a incapsulare la logica aziendale e impedire alla logica aziendale di insinuarsi nei modelli o nei controller. Ciò mantiene la base di codice pulita, leggibile, gestibile e scalabile.
Ecco un esempio di una complessa logica aziendale implementata utilizzando il modello pub/sub e gli oggetti di servizio:
Editore
# 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 # ...
Iscritti
# 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
Utilizzando il modello publish-subscribe, la base di codice viene organizzata in SRSO quasi automaticamente. Inoltre, l'implementazione del codice per flussi di lavoro complessi è facilmente organizzata in base agli eventi, senza sacrificare leggibilità, manutenibilità o scalabilità.
Test
Scomponendo i fat models e i controller e disponendo di molti SRSO, il test della base di codice diventa un processo molto, molto più semplice. Questo è particolarmente vero quando si tratta di test di integrazione e comunicazione tra moduli. I test dovrebbero semplicemente garantire che gli eventi siano pubblicati e ricevuti correttamente.

Wisper ha una gemma di test che aggiunge abbinatori RSpec per facilitare il test di diversi componenti.
Nei due esempi precedenti (esempio Post
e Esempio Order
), il test dovrebbe includere quanto segue:
Editori
# 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
Iscritti
# 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
Tuttavia, esistono alcune limitazioni al test degli eventi pubblicati quando l'editore è il controller.
Se vuoi fare uno sforzo in più, testare anche il carico utile ti aiuterà a mantenere una base di codice ancora migliore.
Come puoi vedere, il test del modello di progettazione pub-sub è semplice. Si tratta solo di garantire che i diversi eventi siano pubblicati e ricevuti correttamente.
Prestazione
Questo è più di un possibile vantaggio. Lo stesso modello di progettazione publish-subscribe non ha un impatto intrinseco importante sulle prestazioni del codice. Tuttavia, come con qualsiasi strumento utilizzato nel codice, gli strumenti per l'implementazione di pub/sub possono avere un grande effetto sulle prestazioni. A volte può essere un effetto negativo, ma a volte può essere molto buono.
Innanzitutto, un esempio di effetto negativo: Redis è una "cache e archivio di valori chiave avanzati. Viene spesso definito un server di struttura dati". Questo popolare strumento supporta il modello pub/sub ed è molto stabile. Tuttavia, se viene utilizzato su un server remoto (non lo stesso server su cui è distribuita l'applicazione Rails), risulterà in un'enorme perdita di prestazioni a causa del sovraccarico della rete.
D'altra parte, Wisper ha vari adattatori per la gestione asincrona degli eventi, come wisper-celluloid, wisper-sidekiq e wisper-activejob. Questi strumenti supportano eventi asincroni ed esecuzioni con thread. che, se applicato in modo appropriato, può aumentare enormemente le prestazioni dell'applicazione.
La linea di fondo
Se stai puntando a fare qualcosa in più in termini di prestazioni, il modello pub/sub potrebbe aiutarti a raggiungerlo. Ma anche se non trovi un aumento delle prestazioni con questo modello di progettazione Rails, ti aiuterà comunque a mantenere il codice organizzato e renderlo più manutenibile. Dopotutto, chi può preoccuparsi delle prestazioni del codice che non può essere mantenuto o che non funziona in primo luogo?
Svantaggi dell'implementazione Pub-Sub
Come per tutte le cose, ci sono anche alcuni possibili inconvenienti nel modello pub-sub.
Accoppiamento sciolto (accoppiamento semantico inflessibile)
Il più grande dei punti di forza del modello pub/sub sono anche i suoi maggiori punti deboli. La struttura dei dati pubblicati (il payload dell'evento) deve essere ben definita e diventa rapidamente piuttosto rigida. Per modificare la struttura dei dati del payload pubblicato, è necessario conoscere tutti gli Abbonati e modificarli anche, oppure assicurarsi che le modifiche siano compatibili con le versioni precedenti. Ciò rende molto più difficile il refactoring del codice dell'editore.
Se vuoi evitarlo, devi essere molto cauto quando definisci il carico utile degli editori. Ovviamente, se disponi di un'ottima suite di test, che testa il payload così come menzionato in precedenza, non devi preoccuparti molto del fatto che il sistema si interrompa dopo aver modificato il payload dell'editore o il nome dell'evento.
Stabilità del bus di messaggistica
Gli editori non sono a conoscenza dello stato dell'abbonato e viceversa. Utilizzando semplici strumenti pub/sub, potrebbe non essere possibile garantire la stabilità del bus di messaggistica stesso e garantire che tutti i messaggi pubblicati siano accodati e consegnati correttamente.
Il numero crescente di messaggi scambiati porta a instabilità nel sistema quando si utilizzano strumenti semplici e potrebbe non essere possibile garantire la consegna a tutti gli abbonati senza alcuni protocolli più sofisticati. A seconda di quanti messaggi vengono scambiati e dei parametri di prestazione che desideri ottenere, potresti prendere in considerazione l'utilizzo di servizi come RabbitMQ, PubNub, Pusher, CloudAMQP, IronMQ o molte altre alternative. Queste alternative forniscono funzionalità extra e sono più stabili di Wisper per i sistemi più complessi. Tuttavia, richiedono anche del lavoro extra per essere implementati. Puoi leggere di più su come funzionano i broker di messaggi qui
Loop di eventi infiniti
Quando il sistema è completamente guidato dagli eventi, dovresti prestare molta attenzione a non avere loop di eventi. Questi loop sono proprio come gli infiniti loop che possono verificarsi nel codice. Tuttavia, sono più difficili da rilevare in anticipo e possono arrestare il sistema. Possono esistere senza il tuo preavviso quando ci sono molti eventi pubblicati e sottoscritti nel sistema.
Conclusione del tutorial sulle rotaie
Il modello publish-subscribe non è un proiettile d'argento per tutti i tuoi problemi Rails e gli odori del codice, ma è un modello di progettazione davvero buono che aiuta a disaccoppiare diversi componenti di sistema e renderlo più manutenibile, leggibile e scalabile.
Se combinato con oggetti di servizio a responsabilità singola (SRSO), pub-sub può anche aiutare a incapsulare la logica aziendale e impedire che problemi aziendali diversi si insinuino nei modelli o nei controller.
Il guadagno in termini di prestazioni dopo l'utilizzo di questo modello dipende principalmente dallo strumento sottostante utilizzato, ma in alcuni casi il guadagno di prestazioni può essere migliorato in modo significativo e nella maggior parte dei casi non danneggerà certamente le prestazioni.
Tuttavia, l'uso del modello pub-sub dovrebbe essere studiato e pianificato con attenzione, perché con il grande potere dell'accoppiamento sciolto deriva la grande responsabilità di mantenere e refactoring i componenti accoppiati in modo lasco.
Dato che gli eventi potrebbero facilmente sfuggire al controllo, una semplice libreria pub/sub potrebbe non garantire la stabilità del broker di messaggi.
Infine, c'è il pericolo di introdurre infiniti loop di eventi che passano inosservati finché non è troppo tardi.
Uso questo modello da quasi un anno ed è difficile per me immaginare di scrivere codice senza di esso. Per me, è il collante che fa in modo che lavori in background, oggetti di servizio, preoccupazioni, controller e modelli comunichino tra loro in modo pulito e funzionino insieme come un fascino.
Spero che tu abbia imparato tanto quanto me dalla revisione di questo codice e che ti senta ispirato a dare al modello Publish-Subscribe una possibilità per rendere la tua applicazione Rails fantastica.
Infine, un enorme grazie a @krisleech per il suo fantastico lavoro nell'implementazione di Wisper.