Das Publish-Subscribe-Muster auf Rails: Ein Implementierungs-Tutorial
Veröffentlicht: 2022-03-11Das Publish-Subscribe-Muster (oder kurz Pub/Sub) ist ein Nachrichtenmuster von Ruby on Rails, bei dem Sender von Nachrichten (Publisher) die Nachrichten nicht so programmieren, dass sie direkt an bestimmte Empfänger (Subscriber) gesendet werden. Stattdessen „veröffentlicht“ der Programmierer Nachrichten (Ereignisse), ohne Kenntnis von eventuell vorhandenen Abonnenten.
In ähnlicher Weise bekunden Abonnenten Interesse an einer oder mehreren Veranstaltungen und erhalten nur Nachrichten, die von Interesse sind, ohne Kenntnis von Herausgebern.
Um dies zu erreichen, empfängt ein Vermittler, der als „Message Broker“ oder „Event Bus“ bezeichnet wird, veröffentlichte Nachrichten und leitet sie dann an die Abonnenten weiter, die für den Empfang registriert sind.
Mit anderen Worten, Pub-Sub ist ein Muster, das verwendet wird, um Nachrichten zwischen verschiedenen Systemkomponenten auszutauschen, ohne dass diese Komponenten etwas über die Identität der anderen wissen.
Dieses Entwurfsmuster ist nicht neu, wird aber von Rails-Entwicklern nicht häufig verwendet. Es gibt viele Tools, mit denen Sie dieses Entwurfsmuster in Ihre Codebasis integrieren können, z. B.:
- Wisper (was ich persönlich bevorzuge und weiter besprechen werde)
- EventBus
- EventBGBus (eine Abspaltung von EventBus)
- RabbitMQ
- Redis
Alle diese Tools haben unterschiedliche zugrunde liegende Pub-Sub-Implementierungen, aber sie alle bieten die gleichen großen Vorteile für eine Rails-Anwendung.
Vorteile der Pub-Sub-Implementierung
Modell-/Controller-Aufblähung reduzieren
Es ist eine gängige Praxis, aber keine Best Practice, einige fette Modelle oder Controller in Ihrer Rails-Anwendung zu haben.
Das Pub/Sub-Muster kann leicht dabei helfen, fette Modelle oder Controller zu zerlegen.
Weniger Rückrufe
Viele miteinander verflochtene Rückrufe zwischen den Modellen zu haben, ist ein bekannter Code-Geruch, und Stück für Stück koppelt es die Modelle eng aneinander, wodurch sie schwieriger zu warten oder zu erweitern sind.
Ein Post -Modell könnte beispielsweise wie folgt aussehen:
# 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 Und der Post Controller könnte etwa so aussehen:
# 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 Wie Sie sehen können, verfügt das Post -Modell über Callbacks, die das Modell sowohl mit dem Feed -Modell als auch mit dem User::NotifyFollowers -Dienst oder -Anliegen eng koppeln. Durch die Verwendung eines beliebigen Pub/Sub-Musters könnte der vorherige Code so umgestaltet werden, dass er etwa wie der folgende lautet, der Wisper verwendet:
# app/models/post.rb class Post # ... field: content, type: String # ... # no callbacks in the models! endHerausgeber veröffentlichen das Ereignis mit dem möglicherweise erforderlichen Ereignisnutzlastobjekt.
# 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 # ... endAbonnenten abonnieren nur die Ereignisse, auf die sie reagieren möchten.
# 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 endEvent Bus registriert die verschiedenen Teilnehmer im System.
# config/initializers/wisper.rb Wisper.subscribe(FeedListener.new) Wisper.subscribe(UserListener.new) In diesem Beispiel hat das Pub-Sub-Muster Rückrufe im Post -Modell vollständig eliminiert und den Modellen geholfen, unabhängig voneinander mit minimalem Wissen über einander zu arbeiten, wodurch eine lockere Kopplung sichergestellt wird. Das Erweitern des Verhaltens auf zusätzliche Aktionen ist nur eine Frage der Verknüpfung mit dem gewünschten Ereignis.
Das Single-Responsibility-Prinzip (SRP)
Das Single-Responsibility-Prinzip ist wirklich hilfreich für die Aufrechterhaltung einer sauberen Codebasis. Das Problem dabei ist, dass die Verantwortung der Klasse manchmal nicht so klar ist, wie sie sein sollte. Dies ist besonders häufig bei MVCs (wie Rails) der Fall.
Modelle sollten Persistenz, Assoziationen und nicht viel mehr handhaben.
Controller sollten Benutzeranforderungen verarbeiten und die Geschäftslogik (Dienstobjekte) umhüllen.
Service-Objekte sollten eine der Verantwortlichkeiten der Geschäftslogik kapseln, einen Einstiegspunkt für externe Dienste bieten oder als Alternative zum Modellieren von Bedenken dienen.
Dank seiner Fähigkeit, die Kopplung zu reduzieren, kann das Pub-Sub-Entwurfsmuster mit Single Responsibility Service Objects (SRSOs) kombiniert werden, um die Kapselung der Geschäftslogik zu unterstützen und zu verhindern, dass sich die Geschäftslogik in die Modelle oder die Controller einschleicht. Dadurch bleibt die Codebasis sauber, lesbar, wartbar und skalierbar.
Hier ist ein Beispiel für eine komplexe Geschäftslogik, die mithilfe des Pub/Sub-Musters und Dienstobjekten implementiert wurde:
Herausgeber
# 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 # ...Abonnenten
# 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 endDurch die Verwendung des Publish-Subscribe-Musters wird die Codebasis fast automatisch in SRSOs organisiert. Darüber hinaus lässt sich die Implementierung von Code für komplexe Workflows einfach um Ereignisse herum organisieren, ohne die Lesbarkeit, Wartbarkeit oder Skalierbarkeit zu beeinträchtigen.
Testen
Durch die Zerlegung der fetten Modelle und Controller und das Vorhandensein vieler SRSOs wird das Testen der Codebasis zu einem viel, viel einfacheren Prozess. Dies gilt insbesondere für Integrationstests und die Kommunikation zwischen den Modulen. Das Testen sollte lediglich sicherstellen, dass Ereignisse korrekt veröffentlicht und empfangen werden.

Wisper verfügt über ein Test-Gem, das RSpec-Matcher hinzufügt, um das Testen verschiedener Komponenten zu erleichtern.
In den beiden vorherigen Beispielen ( Post -Beispiel und Order ) sollte der Test Folgendes umfassen:
Verlag
# 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 endAbonnenten
# 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 endEs gibt jedoch einige Einschränkungen beim Testen der veröffentlichten Ereignisse, wenn der Herausgeber der Controller ist.
Wenn Sie noch einen Schritt weiter gehen möchten, hilft das Testen der Nutzlast dabei, eine noch bessere Codebasis zu erhalten.
Wie Sie sehen können, ist das Testen von Pub-Sub-Designmustern einfach. Es geht lediglich darum sicherzustellen, dass die verschiedenen Veranstaltungen korrekt veröffentlicht und empfangen werden.
Leistung
Dies ist eher ein möglicher Vorteil. Das Publish-Subscribe-Entwurfsmuster selbst hat keine großen inhärenten Auswirkungen auf die Codeleistung. Wie bei jedem Tool, das Sie in Ihrem Code verwenden, können sich die Tools zum Implementieren von Pub/Sub jedoch stark auf die Leistung auswirken. Manchmal kann es eine schlechte Wirkung haben, aber manchmal kann es sehr gut sein.
Zunächst ein Beispiel für einen schlechten Effekt: Redis ist ein „erweiterter Schlüsselwert-Cache und -Speicher. Er wird oft als Datenstrukturserver bezeichnet.“ Dieses beliebte Tool unterstützt das Pub/Sub-Muster und ist sehr stabil. Wenn es jedoch auf einem Remote-Server verwendet wird (nicht auf demselben Server, auf dem die Rails-Anwendung bereitgestellt wird), führt dies zu einem enormen Leistungsverlust aufgrund von Netzwerk-Overhead.
Andererseits verfügt Wisper über verschiedene Adapter für die asynchrone Ereignisbehandlung, wie wisper-celluloid, wisper-sidekiq und wisper-activejob. Diese Tools unterstützen asynchrone Ereignisse und Thread-Ausführungen. die bei richtiger Anwendung die Leistung der Anwendung enorm steigern können.
Das Endergebnis
Wenn Sie die Extrameile an Leistung anstreben, könnte Ihnen das Pub/Sub-Muster dabei helfen, dies zu erreichen. Aber selbst wenn Sie mit diesem Rails-Entwurfsmuster keine Leistungssteigerung feststellen, trägt es dennoch dazu bei, den Code zu organisieren und besser wartbar zu machen. Denn wer kann sich schon Gedanken über die Performance von Code machen, der nicht gewartet werden kann oder gar nicht erst funktioniert?
Nachteile der Pub-Sub-Implementierung
Wie bei allen Dingen gibt es auch beim Pub-Sub-Muster einige mögliche Nachteile.
Lose Kopplung (unflexible semantische Kopplung)
Die größten Stärken des Pub/Sub-Musters sind gleichzeitig auch seine größten Schwächen. Die Struktur der veröffentlichten Daten (die Ereignisnutzlast) muss gut definiert sein und wird schnell ziemlich unflexibel. Um die Datenstruktur der veröffentlichten Nutzlast zu modifizieren, ist es notwendig, alle Abonnenten zu kennen und sie entweder ebenfalls zu modifizieren oder sicherzustellen, dass die Modifikationen mit älteren Versionen kompatibel sind. Dadurch wird das Refactoring von Publisher-Code erheblich erschwert.
Wenn Sie dies vermeiden möchten, müssen Sie bei der Definition der Payload der Publisher besonders vorsichtig sein. Wenn Sie über eine großartige Testsuite verfügen, die die Payload so gut wie zuvor erwähnt testet, müssen Sie sich natürlich keine großen Sorgen darüber machen, dass das System ausfällt, nachdem Sie die Payload oder den Ereignisnamen des Herausgebers geändert haben.
Stabilität des Messaging-Busses
Publisher haben keine Kenntnis vom Status des Abonnenten und umgekehrt. Mit einfachen Pub/Sub-Tools ist es möglicherweise nicht möglich, die Stabilität des Messaging-Busses selbst sicherzustellen und sicherzustellen, dass alle veröffentlichten Nachrichten korrekt in die Warteschlange gestellt und zugestellt werden.
Die zunehmende Anzahl der ausgetauschten Nachrichten führt bei Verwendung einfacher Tools zu Instabilitäten im System, und es ist möglicherweise nicht möglich, die Zustellung an alle Teilnehmer ohne einige ausgefeiltere Protokolle sicherzustellen. Je nachdem, wie viele Nachrichten ausgetauscht werden und welche Leistungsparameter Sie erreichen möchten, können Sie Dienste wie RabbitMQ, PubNub, Pusher, CloudAMQP, IronMQ oder viele, viele andere Alternativen in Betracht ziehen. Diese Alternativen bieten zusätzliche Funktionalität und sind für komplexere Systeme stabiler als Wisper. Sie erfordern jedoch auch einige zusätzliche Arbeit für die Implementierung. Hier können Sie mehr darüber lesen, wie Message Broker funktionieren
Endlose Ereignisschleifen
Wenn das System vollständig von Ereignissen gesteuert wird, sollten Sie besonders darauf achten, keine Ereignisschleifen zu haben. Diese Schleifen sind genau wie die Endlosschleifen, die im Code vorkommen können. Sie sind jedoch schwerer im Voraus zu erkennen und können Ihr System zum Erliegen bringen. Sie können ohne Ihre Benachrichtigung existieren, wenn viele Ereignisse im System veröffentlicht und abonniert sind.
Abschluss des Rails-Tutorials
Das Publish-Subscribe-Muster ist keine Wunderwaffe für all Ihre Rails-Probleme und Code-Gerüche, aber es ist ein wirklich gutes Designmuster, das dabei hilft, verschiedene Systemkomponenten zu entkoppeln und es wartbarer, lesbarer und skalierbarer zu machen.
In Kombination mit Single Responsibility Service Objects (SRSOs) kann Pub-Sub auch wirklich dabei helfen, die Geschäftslogik zu kapseln und zu verhindern, dass sich verschiedene Geschäftsbelange in die Modelle oder die Controller einschleichen.
Der Leistungsgewinn nach der Verwendung dieses Musters hängt hauptsächlich vom verwendeten zugrunde liegenden Tool ab, aber der Leistungsgewinn kann in einigen Fällen erheblich verbessert werden, und in den meisten Fällen wird die Leistung sicherlich nicht beeinträchtigt.
Die Verwendung des Pub-Sub-Musters sollte jedoch sorgfältig untersucht und geplant werden, da mit der großen Macht der losen Kopplung die große Verantwortung für die Wartung und Umgestaltung von lose gekoppelten Komponenten einhergeht.
Da Ereignisse leicht außer Kontrolle geraten können, gewährleistet eine einfache Pub/Sub-Bibliothek möglicherweise nicht die Stabilität des Nachrichtenbrokers.
Und schließlich besteht die Gefahr, endlose Ereignisschleifen einzuführen, die unbemerkt bleiben, bis es zu spät ist.
Ich verwende dieses Muster jetzt seit fast einem Jahr, und es fällt mir schwer, mir vorzustellen, Code ohne es zu schreiben. Für mich ist es der Kitt, der Hintergrundjobs, Serviceobjekte, Anliegen, Controller und Models sauber miteinander kommunizieren lässt und wie am Schnürchen zusammenarbeitet.
Ich hoffe, Sie haben durch die Überprüfung dieses Codes genauso viel gelernt wie ich und fühlen sich inspiriert, dem Publish-Subscribe-Muster eine Chance zu geben, Ihre Rails-Anwendung großartig zu machen.
Abschließend ein großes Dankeschön an @krisleech für seine großartige Arbeit bei der Implementierung von Wisper.
