Wzorzec publikuj-subskrybuj w Rails: samouczek implementacji
Opublikowany: 2022-03-11Wzorzec publikowania-subskrypcji (lub w skrócie pub/sub) jest wzorcem komunikatów Ruby on Rails, w którym nadawcy komunikatów (wydawcy) nie programują komunikatów, które mają być wysyłane bezpośrednio do określonych odbiorców (subskrybentów). Zamiast tego programista „publikuje” wiadomości (zdarzenia), bez wiedzy o ewentualnych subskrybentach.
Podobnie subskrybenci wyrażają zainteresowanie jednym lub większą liczbą wydarzeń i otrzymują tylko interesujące wiadomości, bez wiedzy jakichkolwiek wydawców.
W tym celu pośrednik, zwany „brokerem wiadomości” lub „szyną zdarzeń”, odbiera opublikowane wiadomości, a następnie przekazuje je subskrybentom, którzy są zarejestrowani w celu ich otrzymywania.
Innymi słowy, pub-sub to wzorzec używany do komunikowania komunikatów między różnymi komponentami systemu bez wiedzy tych komponentów o wzajemnej tożsamości.
Ten wzorzec projektowy nie jest nowy, ale nie jest powszechnie używany przez programistów Rails. Istnieje wiele narzędzi, które pomagają włączyć ten wzorzec projektowy do bazy kodu, na przykład:
- Wisper (który osobiście wolę i omówię dalej)
- EventBus
- EventBGBus (widelec EventBus)
- KrólikMQ
- Redis
Wszystkie te narzędzia mają różne podstawowe implementacje pub-sub, ale wszystkie oferują te same główne zalety dla aplikacji Rails.
Zalety wdrożenia Pub-Sub
Zmniejszenie rozdęcia modelu/kontrolera
Powszechną praktyką, ale nie najlepszą, jest posiadanie niektórych grubych modeli lub kontrolerów w aplikacji Railsowej.
Wzorzec pub/sub może z łatwością pomóc w rozłożeniu grubych modeli lub kontrolerów.
Mniej oddzwonień
Posiadanie wielu powiązanych ze sobą wywołań zwrotnych między modelami jest dobrze znanym zapachem kodu, który krok po kroku ściśle łączy modele, czyniąc je trudniejszymi do utrzymania lub rozszerzenia.
Na przykład model Post
może wyglądać tak:
# 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
Kontroler Post
może wyglądać mniej więcej tak:
# 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
Jak widać, model Post
ma wywołania zwrotne, które ściśle łączą model zarówno z modelem kanału, jak i usługą lub problemem User::NotifyFollowers
Feed
Używając dowolnego wzorca pub/sub, poprzedni kod może zostać zrefaktoryzowany tak, aby wyglądał podobnie do następującego, który wykorzystuje Wisper:
# app/models/post.rb class Post # ... field: content, type: String # ... # no callbacks in the models! end
Wydawcy publikują zdarzenie z obiektem ładunku zdarzenia, który może być potrzebny.
# 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
Subskrybenci subskrybują tylko te wydarzenia, na które chcą odpowiedzieć.
# 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 rejestruje różnych abonentów w systemie.
# config/initializers/wisper.rb Wisper.subscribe(FeedListener.new) Wisper.subscribe(UserListener.new)
W tym przykładzie wzorzec pub-sub całkowicie wyeliminował wywołania zwrotne w modelu Post
i pomógł modelom pracować niezależnie od siebie przy minimalnej wiedzy o sobie, zapewniając luźne sprzężenie. Rozszerzenie zachowania na dodatkowe akcje to tylko kwestia podpięcia się do pożądanego zdarzenia.
Zasada pojedynczej odpowiedzialności (SRP)
Zasada pojedynczej odpowiedzialności jest naprawdę pomocna w utrzymaniu czystej bazy kodu. Problem w trzymaniu się tego polega na tym, że czasami odpowiedzialność klasy nie jest tak jasna, jak powinna. Jest to szczególnie powszechne w przypadku MVC (takich jak Rails).
Modele powinny radzić sobie z trwałością, skojarzeniami i niewiele więcej.
Kontrolery powinny obsługiwać żądania użytkowników i być opakowaniem wokół logiki biznesowej (obiekty usług).
Obiekty usług powinny zawierać jeden z obowiązków logiki biznesowej, stanowić punkt wejścia dla usług zewnętrznych lub działać jako alternatywa dla problemów modelowych.
Dzięki możliwości ograniczenia sprzężenia wzorzec projektowy pub-sub można łączyć z obiektami usług o pojedynczej odpowiedzialności (SRSO), aby pomóc w hermetyzacji logiki biznesowej i uniemożliwić jej wkradanie się do modeli lub kontrolerów. Dzięki temu baza kodu jest czysta, czytelna, możliwa do utrzymania i skalowalna.
Oto przykład złożonej logiki biznesowej zaimplementowanej przy użyciu wzorca pub/sub i obiektów usług:
Wydawca
# 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 # ...
Subskrybenci
# 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
Używając wzorca publikuj-subskrybuj, baza kodu jest organizowana w SRSO prawie automatycznie. Co więcej, implementacja kodu dla złożonych przepływów pracy jest łatwo organizowana wokół zdarzeń, bez poświęcania czytelności, łatwości konserwacji czy skalowalności.
Testowanie
Poprzez dekompozycję grubych modeli i kontrolerów oraz posiadanie wielu SRSO, testowanie bazy kodu staje się znacznie łatwiejszym procesem. Dotyczy to w szczególności testów integracyjnych i komunikacji międzymodułowej. Testowanie powinno po prostu zapewnić, że zdarzenia są publikowane i odbierane prawidłowo.

Wisper ma klejnot testowy, który dodaje elementy dopasowujące RSpec, aby ułatwić testowanie różnych komponentów.
W poprzednich dwóch przykładach (przykład Post
i przykład Order
) testowanie powinno obejmować:
Wydawcy
# 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
Subskrybenci
# 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
Istnieją jednak pewne ograniczenia dotyczące testowania opublikowanych zdarzeń, gdy wydawca jest kontrolerem.
Jeśli chcesz pójść o krok dalej, przetestowanie ładunku pomoże utrzymać jeszcze lepszą bazę kodu.
Jak widać, testowanie wzorców projektowych pub-sub jest proste. To tylko kwestia upewnienia się, że różne wydarzenia są prawidłowo publikowane i odbierane.
Występ
To jest bardziej możliwa zaleta. Sam wzorzec projektowy publikuj-subskrybuj nie ma dużego nieodłącznego wpływu na wydajność kodu. Jednak, podobnie jak w przypadku każdego narzędzia, którego używasz w swoim kodzie, narzędzia do implementacji pub/sub mogą mieć duży wpływ na wydajność. Czasami może to być zły efekt, ale czasami może być bardzo dobry.
Po pierwsze, przykład złego efektu: Redis to „zaawansowana pamięć podręczna i przechowywanie kluczy i wartości. Często określany jest mianem serwera struktury danych.” To popularne narzędzie obsługuje wzorzec pub/sub i jest bardzo stabilne. Jednakże, jeśli jest używany na zdalnym serwerze (nie na tym samym serwerze, na którym jest wdrożona aplikacja Rails), spowoduje to ogromną utratę wydajności z powodu obciążenia sieciowego.
Z drugiej strony Wisper oferuje różne adaptery do obsługi zdarzeń asynchronicznych, takie jak wisper-celluloid, wisper-sidekiq i wisper-activejob. Te narzędzia obsługują zdarzenia asynchroniczne i wykonania wątkowe. które, jeśli zostaną odpowiednio zastosowane, mogą znacznie zwiększyć wydajność aplikacji.
Dolna linia
Jeśli dążysz do uzyskania dodatkowej mili w wydajności, wzorzec pub/sub może pomóc ci to osiągnąć. Ale nawet jeśli nie znajdziesz wzrostu wydajności dzięki temu wzorcowi projektowemu Rails, nadal pomoże to w utrzymaniu porządku w kodzie i sprawi, że będzie on łatwiejszy w utrzymaniu. W końcu, kto może martwić się wydajnością kodu, którego nie można utrzymać lub który nie działa w pierwszej kolejności?
Wady implementacji Pub-Sub
Podobnie jak w przypadku wszystkich rzeczy, istnieje również kilka możliwych wad wzorca pub-sub.
Luźne sprzężenie (nieelastyczne sprzężenie semantyczne)
Największą mocną stroną wzoru pub/sub są jednocześnie jego największe słabości. Struktura publikowanych danych (ładunek zdarzeń) musi być dobrze zdefiniowana i szybko staje się mało elastyczna. Aby zmodyfikować strukturę danych publikowanego ładunku, konieczne jest poznanie wszystkich Abonentów, a także ich zmodyfikowanie lub upewnienie się, że modyfikacje są kompatybilne ze starszymi wersjami. To znacznie utrudnia refaktoryzację kodu programu Publisher.
Jeśli chcesz tego uniknąć, musisz zachować szczególną ostrożność podczas definiowania ładunku wydawców. Oczywiście, jeśli masz świetny zestaw testów, który testuje ładunek, jak wspomniano wcześniej, nie musisz się zbytnio martwić, że system przestanie działać po zmianie ładunku lub nazwy zdarzenia wydawcy.
Komunikaty o stabilności magistrali
Wydawcy nie mają wiedzy o statusie subskrybenta i odwrotnie. Przy użyciu prostych narzędzi publikowania/subskrypcji może nie być możliwe zapewnienie stabilności samej magistrali komunikacyjnej oraz zapewnienie, że wszystkie opublikowane wiadomości są prawidłowo umieszczane w kolejce i dostarczane.
Rosnąca liczba wymienianych wiadomości prowadzi do niestabilności systemu przy użyciu prostych narzędzi i może nie być możliwe zapewnienie dostarczenia do wszystkich abonentów bez bardziej zaawansowanych protokołów. W zależności od liczby wymienianych wiadomości i parametrów wydajności, które chcesz osiągnąć, możesz rozważyć skorzystanie z usług takich jak RabbitMQ, PubNub, Pusher, CloudAMQP, IronMQ lub wielu innych alternatyw. Te alternatywy zapewniają dodatkową funkcjonalność i są bardziej stabilne niż Wisper dla bardziej złożonych systemów. Jednak ich wdrożenie wymaga również dodatkowej pracy. Możesz przeczytać więcej o tym, jak działają brokerzy wiadomości tutaj
Nieskończone pętle zdarzeń
Gdy system jest całkowicie napędzany zdarzeniami, należy zachować szczególną ostrożność, aby nie mieć pętli zdarzeń. Te pętle są jak nieskończone pętle, które mogą wystąpić w kodzie. Są one jednak trudniejsze do wykrycia z wyprzedzeniem i mogą spowodować zatrzymanie systemu. Mogą istnieć bez powiadomienia, gdy w systemie opublikowano i subskrybowano wiele wydarzeń.
Podsumowanie samouczka Rails
Wzorzec publikuj-subskrybuj nie jest srebrną kulą dla wszystkich twoich problemów z Railsami i zapachów kodu, ale jest naprawdę dobrym wzorcem projektowym, który pomaga oddzielić różne komponenty systemu i uczynić go łatwiejszym w utrzymaniu, czytelnym i skalowalnym.
W połączeniu z obiektami usług o pojedynczej odpowiedzialności (SRSO), pub-sub może również naprawdę pomóc w hermetyzacji logiki biznesowej i zapobieganiu wkradaniu się różnych problemów biznesowych do modeli lub kontrolerów.
Wzrost wydajności po użyciu tego wzorca zależy głównie od używanego narzędzia, ale w niektórych przypadkach można go znacznie poprawić, a w większości przypadków z pewnością nie zaszkodzi wydajności.
Jednak użycie wzorca pub-sub powinno być dokładnie przestudiowane i zaplanowane, ponieważ z wielką mocą luźnego sprzężenia wiąże się wielka odpowiedzialność za utrzymanie i refaktoryzację luźno połączonych komponentów.
Ponieważ zdarzenia mogą łatwo wymknąć się spod kontroli, prosta biblioteka pub/sub może nie zapewnić stabilności brokera komunikatów.
I wreszcie, istnieje niebezpieczeństwo wprowadzenia nieskończonych pętli zdarzeń, które pozostaną niezauważone, dopóki nie będzie za późno.
Używam tego wzorca już prawie rok i ciężko mi sobie wyobrazić pisanie kodu bez niego. Dla mnie jest to klej, który sprawia, że zadania w tle, obiekty usługowe, obawy, kontrolery i modele komunikują się ze sobą czysto i współpracują ze sobą jak urok.
Mam nadzieję, że nauczyłeś się tak wiele, jak ja, przeglądając ten kod, i że czujesz się zainspirowany, aby dać wzorcowi publikowania-subskrypcji szansę uczynienia Twojej aplikacji Rails niesamowitymi.
Na koniec wielkie podziękowania dla @krisleech za jego niesamowitą pracę przy wdrażaniu Wisper.