Modelul de publicare-abonare pe șine: un tutorial de implementare
Publicat: 2022-03-11Modelul publish-subscribe (sau pub/sub, pe scurt) este un model de mesagerie Ruby on Rails în care expeditorii de mesaje (editorii), nu programează mesajele să fie trimise direct către anumiți receptori (abonați). În schimb, programatorul „publică” mesaje (evenimente), fără să știe vreun abonat care ar putea exista.
În mod similar, abonații își exprimă interes pentru unul sau mai multe evenimente și primesc doar mesaje care sunt de interes, fără nicio cunoștință de către vreun editor.
Pentru a realiza acest lucru, un intermediar, numit „broker de mesaje” sau „autobuz de evenimente”, primește mesajele publicate și apoi le transmite acelor abonați care sunt înregistrați pentru a le primi.
Cu alte cuvinte, pub-sub este un model folosit pentru a comunica mesaje între diferite componente ale sistemului fără ca aceste componente să știe ceva despre identitatea celuilalt.
Acest model de design nu este nou, dar nu este folosit în mod obișnuit de dezvoltatorii Rails. Există multe instrumente care ajută la încorporarea acestui model de design în baza de cod, cum ar fi:
- Wisper (pe care eu personal îl prefer și îl voi discuta mai departe)
- EventBus
- EventBGBus (o bifurcație a EventBus)
- RabbitMQ
- Redis
Toate aceste instrumente au implementări diferite subiacente pub-sub, dar toate oferă aceleași avantaje majore pentru o aplicație Rails.
Avantajele implementării Pub-Sub
Reducerea balonării modelului/controlerului
Este o practică obișnuită, dar nu cea mai bună practică, să aveți câteva modele sau controlere grase în aplicația dvs. Rails.
Modelul pub/sub poate ajuta cu ușurință la descompunerea modelelor grase sau controlerelor.
Mai puține apeluri inverse
A avea o mulțime de apeluri inversate între modele este un miros de cod bine-cunoscut și, încetul cu încetul, cuplează strâns modelele, făcându-le mai greu de întreținut sau extins.
De exemplu, un model Post
ar putea arăta astfel:
# 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
Și controlerul Post
ar putea arăta cam așa:
# 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
După cum puteți vedea, modelul Post
are apeluri care cuplează strâns modelul atât cu modelul Feed
, cât și cu serviciul sau preocuparea User::NotifyFollowers
. Prin utilizarea oricărui model pub/sub, codul anterior ar putea fi refactorizat pentru a fi ceva de genul următor, care utilizează Wisper:
# app/models/post.rb class Post # ... field: content, type: String # ... # no callbacks in the models! end
Editorii publică evenimentul cu obiectul de sarcină utilă a evenimentului care ar putea fi necesar.
# 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
Abonații se abonează doar la evenimentele la care doresc să răspundă.
# 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 înregistrează diferiții abonați din sistem.
# config/initializers/wisper.rb Wisper.subscribe(FeedListener.new) Wisper.subscribe(UserListener.new)
În acest exemplu, modelul pub-sub a eliminat complet apelurile în modelul Post
și a ajutat modelele să funcționeze independent unul de celălalt, cu cunoștințe minime unul despre celălalt, asigurând o cuplare slabă. Extinderea comportamentului la acțiuni suplimentare este doar o chestiune de agățare la evenimentul dorit.
Principiul responsabilității unice (SRP)
Principiul responsabilității unice este cu adevărat util pentru menținerea unei baze de cod curate. Problema în a rămâne cu ea este că uneori responsabilitatea clasei nu este atât de clară pe cât ar trebui să fie. Acest lucru este obișnuit mai ales când vine vorba de MVC-uri (cum ar fi șine).
Modelele ar trebui să se ocupe de persistență, asocieri și nu multe altele.
Controlorii ar trebui să se ocupe de solicitările utilizatorilor și să fie un înveliș în jurul logicii de afaceri (Obiecte de serviciu).
Obiectele de serviciu ar trebui să încapsuleze una dintre responsabilitățile logicii de afaceri, să ofere un punct de intrare pentru serviciile externe sau să acționeze ca o alternativă la preocupările de model.
Datorită puterii sale de a reduce cuplarea, modelul de proiectare pub-sub poate fi combinat cu obiecte de serviciu cu responsabilitate unică (SRSO) pentru a ajuta la încapsularea logicii de afaceri și a interzice logicii de afaceri să se strecoare fie în modele, fie în controlere. Acest lucru menține baza de cod curată, lizibilă, întreținută și scalabilă.
Iată un exemplu de logică de afaceri complexă implementată folosind modelul pub/sub și obiectele de serviciu:
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 # ...
Abonați
# 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
Prin utilizarea modelului de publicare-abonare, baza de cod se organizează aproape automat în SRSO. Mai mult, implementarea codului pentru fluxuri de lucru complexe este ușor organizată în jurul evenimentelor, fără a sacrifica lizibilitatea, mentenabilitatea sau scalabilitatea.
Testare
Prin descompunerea modelelor și controlerelor grase și având o mulțime de SRSO, testarea bazei de cod devine un proces mult, mult mai ușor. Acesta este în special cazul când vine vorba de testarea integrării și comunicarea între module. Testarea ar trebui pur și simplu să asigure că evenimentele sunt publicate și primite corect.

Wisper are o bijuterie de testare care adaugă potriviri RSpec pentru a ușura testarea diferitelor componente.
În cele două exemple anterioare (exemplu de Post
și exemplu de Order
), testarea ar trebui să includă următoarele:
Editorii
# 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
Abonați
# 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
Cu toate acestea, există unele limitări la testarea evenimentelor publicate atunci când editorul este controlorul.
Dacă doriți să mergeți mai departe, testarea sarcinii utile va ajuta la menținerea unei baze de cod și mai bune.
După cum puteți vedea, testarea modelelor de design pub-sub este simplă. Este vorba doar de a vă asigura că diferitele evenimente sunt corect publicate și primite.
Performanţă
Acesta este mai mult un posibil avantaj. Modelul de proiectare de publicare-abonare în sine nu are un impact inerent major asupra performanței codului. Cu toate acestea, ca și în cazul oricărui instrument pe care îl utilizați în codul dvs., instrumentele pentru implementarea pub/sub pot avea un efect mare asupra performanței. Uneori poate fi un efect rău, dar uneori poate fi foarte bun.
În primul rând, un exemplu de efect negativ: Redis este un „cache și stocare avansat cheie-valoare. Este adesea menționat ca un server de structură de date.” Acest instrument popular acceptă modelul pub/sub și este foarte stabil. Cu toate acestea, dacă este utilizat pe un server la distanță (nu același server pe care este implementată aplicația Rails), va avea ca rezultat o pierdere uriașă de performanță din cauza supraîncărcării rețelei.
Pe de altă parte, Wisper are diverse adaptoare pentru gestionarea asincronă a evenimentelor, cum ar fi wisper-celluloid, wisper-sidekiq și wisper-activejob. Aceste instrumente acceptă evenimente asincrone și execuții threaded. care, dacă este aplicat corespunzător, poate crește enorm performanța aplicației.
Concluzia
Dacă țintiți pentru performanță suplimentară, modelul pub/sub vă poate ajuta să ajungeți la el. Dar chiar dacă nu găsiți o creștere a performanței cu acest model de design Rails, va ajuta totuși să mențineți codul organizat și să îl faceți mai ușor de întreținut. La urma urmei, cine se poate îngrijora de performanța unui cod care nu poate fi menținut sau care nu funcționează în primul rând?
Dezavantajele implementării Pub-Sub
Ca și în cazul tuturor lucrurilor, există și câteva posibile dezavantaje ale modelului pub-sub.
Cuplaj liber (cuplaj semantic inflexibil)
Cele mai mari puncte forte ale modelului pub/sub sunt și cele mai mari puncte slabe ale acestuia. Structura datelor publicate (sarcina utilă a evenimentului) trebuie să fie bine definită și devine rapid destul de inflexibilă. Pentru a modifica structura datelor din sarcina utilă publicată, este necesar să cunoașteți toți Abonații și fie să le modificați și pe aceștia, fie să vă asigurați că modificările sunt compatibile cu versiunile mai vechi. Acest lucru face ca refactorizarea codului Publisher să fie mult mai dificilă.
Dacă doriți să evitați acest lucru, trebuie să fiți foarte precaut atunci când definiți sarcina utilă a editorilor. Desigur, dacă aveți o suită de testare grozavă, care testează sarcina utilă, așa cum s-a menționat anterior, nu trebuie să vă faceți griji cu privire la defectarea sistemului după ce schimbați sarcina utilă sau numele evenimentului editorului.
Stabilitate autobuz de mesagerie
Editorii nu au cunoștințe despre statutul abonatului și invers. Folosind instrumente simple pub/sub, este posibil să nu fie posibil să se asigure stabilitatea magistralei de mesagerie în sine și să se asigure că toate mesajele publicate sunt puse în coadă și livrate corect.
Numărul tot mai mare de mesaje schimbate duce la instabilități în sistem atunci când se utilizează instrumente simple și este posibil să nu fie posibil să se asigure livrarea către toți abonații fără niște protocoale mai sofisticate. În funcție de câte mesaje sunt schimbate și de parametrii de performanță pe care doriți să îi atingeți, puteți lua în considerare utilizarea unor servicii precum RabbitMQ, PubNub, Pusher, CloudAMQP, IronMQ sau multe alte alternative. Aceste alternative oferă funcționalitate suplimentară și sunt mai stabile decât Wisper pentru sisteme mai complexe. Cu toate acestea, ele necesită și ceva muncă suplimentară pentru implementare. Puteți citi mai multe despre cum funcționează brokerii de mesaje aici
Bucle infinite de evenimente
Când sistemul este condus complet de evenimente, ar trebui să fiți foarte precaut să nu aveți bucle de evenimente. Aceste bucle sunt la fel ca buclele infinite care se pot întâmpla în cod. Cu toate acestea, sunt mai greu de detectat din timp și vă pot opri sistemul. Ele pot exista fără notificarea dvs. atunci când există multe evenimente publicate și abonate în sistem.
Concluzie Tutorial Rails
Modelul de publicare-abonare nu este un glonț de argint pentru toate problemele tale Rails și mirosurile de cod, dar este un model de design foarte bun care ajută la decuplarea diferitelor componente ale sistemului și la face mai ușor de întreținut, lizibil și scalabil.
Atunci când sunt combinate cu obiecte de serviciu cu responsabilitate unică (SRSO), pub-sub poate ajuta, de asemenea, cu adevărat la încapsularea logicii de afaceri și la prevenirea diferitelor preocupări de afaceri să se strecoare în modele sau controlori.
Câștigul de performanță după utilizarea acestui model depinde în principal de instrumentul de bază utilizat, dar câștigul de performanță poate fi îmbunătățit semnificativ în unele cazuri și, în majoritatea cazurilor, cu siguranță nu va afecta performanța.
Cu toate acestea, utilizarea modelului pub-sub ar trebui studiată și planificată cu atenție, deoarece odată cu marea putere a cuplării libere vine și marea responsabilitate a menținerii și refactorizării componentelor cuplate liber.
Deoarece evenimentele ar putea scăpa cu ușurință de sub control, este posibil ca o simplă bibliotecă pub/sub să nu asigure stabilitatea brokerului de mesaje.
Și, în sfârșit, există pericolul introducerii unor bucle infinite de evenimente care trec neobservate până nu este prea târziu.
Folosesc acest model de aproape un an și îmi este greu să-mi imaginez că scriu cod fără el. Pentru mine, este lipiciul care face ca lucrările de fundal, obiectele de service, preocupările, controlerele și modelele să comunice între ele în mod curat și să lucreze împreună ca un farmec.
Sper că ați învățat la fel de mult ca și mine din revizuirea acestui cod și că vă simțiți inspirat să oferi șansa de a vă face aplicația Rails minunată modelului Publicare-Abonare.
În cele din urmă, un mare mulțumire lui @krisleech pentru munca sa minunată în implementarea Wisper.