Railsでのパブリッシュ/サブスクライブパターン:実装チュートリアル

公開: 2022-03-11

パブリッシュ/サブスクライブパターン(または略してpub / sub)は、メッセージの送信者(パブリッシャー)が特定の受信者(サブスクライバー)に直接送信されるようにメッセージをプログラムしないRubyonRailsメッセージングパターンです。 代わりに、プログラマーは、サブスクライバーの知識がなくても、メッセージ(イベント)を「公開」します。

同様に、サブスクライバーは1つ以上のイベントに関心を示し、発行者の知識がなくても、関心のあるメッセージのみを受信します。

これを実現するために、「メッセージブローカー」または「イベントバス」と呼ばれる仲介者は、公開されたメッセージを受信し、それらを受信するように登録されているサブスクライバーに転送します。

言い換えると、pub-subは、異なるシステムコンポーネント間でメッセージを通信するために使用されるパターンであり、これらのコンポーネントは互いのIDについて何も知りません。

このRailsチュートリアルでは、パブリッシュ/サブスクライブのデザインパターンがこの図に示されています。

このデザインパターンは新しいものではありませんが、Rails開発者によって一般的に使用されることはありません。 このデザインパターンをコードベースに組み込むのに役立つツールはたくさんあります。たとえば、次のようなものです。

  • ウィスパー(私は個人的に好み、それについてさらに議論します)
  • EventBus
  • EventBGBus(EventBusのフォーク)
  • RabbitMQ
  • Redis

これらのツールはすべて、基盤となるpub-subの実装が異なりますが、Railsアプリケーションに対して同じ主要な利点を提供します。

Pub-Sub実装の利点

モデル/コントローラーの肥大化を減らす

Railsアプリケーションにいくつかのファットモデルまたはコントローラーを含めることは一般的な方法ですが、ベストプラクティスではありません。

pub / subパターンは、ファットモデルまたはコントローラーを簡単に分解するのに役立ちます。

コールバックが少ない

モデル間に多くの絡み合ったコールバックがあることは、よく知られているコードの臭いであり、少しずつモデルを緊密に結合して、モデルの保守や拡張を困難にします。

たとえば、 Postモデルは次のようになります。

 # 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

また、 Postコントローラーは次のようになります。

 # 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

ご覧のとおり、 Postモデルには、モデルをFeedモデルとUser::NotifyFollowersサービスまたは懸念事項の両方に緊密に結合するコールバックがあります。 任意のpub/subパターンを使用することにより、前のコードをリファクタリングして、Wisperを使用する次のようなものにすることができます。

 # app/models/post.rb class Post # ... field: content, type: String # ... # no callbacks in the models! end

パブリッシャーは、必要になる可能性のあるイベントペイロードオブジェクトを使用してイベントを公開します。

 # 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

サブスクライバーは、応答したいイベントのみをサブスクライブします。

 # 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

イベントバスは、システムにさまざまな加入者を登録します。

 # config/initializers/wisper.rb Wisper.subscribe(FeedListener.new) Wisper.subscribe(UserListener.new)

この例では、pub-subパターンにより、 Postモデルのコールバックが完全に排除され、モデルが互いに最小限の知識で互いに独立して動作するようになり、結合が緩くなりました。 動作を追加のアクションに拡張することは、目的のイベントにフックするだけの問題です。

単一責任原則(SRP)

単一責任の原則は、クリーンなコードベースを維持するのに非常に役立ちます。 それに固執する際の問題は、クラスの責任が本来あるべきほど明確でない場合があるということです。 これは、MVC(Railsなど)に関しては特に一般的です。

モデルは、永続性、関連付けなどを処理する必要があります。

コントローラは、ユーザーリクエストを処理し、ビジネスロジック(サービスオブジェクト)のラッパーである必要があります。

サービスオブジェクトは、ビジネスロジックの責任の1つをカプセル化するか、外部サービスのエントリポイントを提供するか、モデルの懸念事項の代替として機能する必要があります。

結合を減らす力のおかげで、pub-subデザインパターンを単一責任サービスオブジェクト(SRSO)と組み合わせて、ビジネスロジックをカプセル化し、ビジネスロジックがモデルまたはコントローラーに忍び寄るのを防ぐことができます。 これにより、コードベースがクリーンで読みやすく、保守可能でスケーラブルに保たれます。

pub/subパターンとサービスオブジェクトを使用して実装されたいくつかの複雑なビジネスロジックの例を次に示します。

出版社

# 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 # ...

サブスクライバー

# 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

パブリッシュ/サブスクライブパターンを使用することにより、コードベースはほぼ自動的にSRSOに編成されます。 さらに、複雑なワークフローのコードの実装は、可読性、保守性、またはスケーラビリティを犠牲にすることなく、イベントを中心に簡単に編成できます。

テスト

ファットモデルとコントローラーを分解し、SRSOを多数持つことで、コードベースのテストははるかに簡単なプロセスになります。 これは、統合テストとモジュール間通信に関して特に当てはまります。 テストでは、イベントが正しく公開および受信されることを確認するだけです。

Wisperには、さまざまなコンポーネントのテストを容易にするためにRSpecマッチャーを追加するテストジェムがあります。

前の2つの例( Postの例とOrderの例)では、テストに次のものを含める必要があります。

出版社

# 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

サブスクライバー

# 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

ただし、発行者がコントローラーである場合、発行されたイベントのテストにはいくつかの制限があります。

さらに一歩進んだ場合は、ペイロードもテストしておくと、さらに優れたコードベースを維持するのに役立ちます。

ご覧のとおり、pub-subデザインパターンのテストは簡単です。 さまざまなイベントが正しく公開および受信されるようにするだけです。

パフォーマンス

これは、より多くの可能性のある利点です。 パブリッシュ/サブスクライブのデザインパターン自体は、コードのパフォーマンスに大きな固有の影響を与えません。 ただし、コードで使用する他のツールと同様に、pub/subを実装するためのツールはパフォーマンスに大きな影響を与える可能性があります。 悪い影響を与えることもありますが、非常に良いこともあります。

まず、悪影響の例:Redisは「高度なKey-Valueキャッシュおよびストア」です。 多くの場合、データ構造サーバーと呼ばれます。」 この人気のあるツールは、pub / subパターンをサポートし、非常に安定しています。 ただし、リモートサーバー(Railsアプリケーションが展開されているサーバーと同じサーバーではない)で使用すると、ネットワークのオーバーヘッドが原因でパフォーマンスが大幅に低下します。

一方、Wisperには、wisper-celluloid、wisper-sidekiq、wisper-activejobなど、非同期イベント処理用のさまざまなアダプターがあります。 これらのツールは、非同期イベントとスレッド化された実行をサポートします。 これを適切に適用すると、アプリケーションのパフォーマンスを大幅に向上させることができます。

結論

パフォーマンスをさらに向上させることを目指している場合は、pub/subパターンがそれに到達するのに役立つ可能性があります。 ただし、このRailsデザインパターンでパフォーマンスの向上が見られない場合でも、コードを整理し、保守しやすくするのに役立ちます。 結局のところ、維持できない、またはそもそも機能しないコードのパフォーマンスについて誰が心配することができますか?

Pub-Sub実装のデメリット

すべてのものと同様に、pub-subパターンにもいくつかの欠点があります。

ルーズカップリング(柔軟性のないセマンティックカップリング)

pub / subパターンの最大の長所は、最大の短所でもあります。 公開されるデータの構造(イベントペイロード)は明確に定義されている必要があり、すぐに柔軟性がなくなります。 公開されたペイロードのデータ構造を変更するには、すべてのサブスクライバーについて知っておく必要があります。また、サブスクライバーも変更するか、変更が古いバージョンと互換性があることを確認する必要があります。 これにより、パブリッシャーコードのリファクタリングがはるかに困難になります。

これを回避したい場合は、パブリッシャーのペイロードを定義するときに特に注意する必要があります。 もちろん、前述のようにペイロードをテストする優れたテストスイートがある場合は、発行元のペイロードまたはイベント名を変更した後にシステムがダウンすることをあまり心配する必要はありません。

メッセージングバスの安定性

パブリッシャーはサブスクライバーのステータスを認識しておらず、その逆も同様です。 単純なpub/subツールを使用すると、メッセージングバス自体の安定性を確保したり、公開されたすべてのメッセージが正しくキューに入れられて配信されたりすることを保証できない場合があります。

交換されるメッセージの数が増えると、単純なツールを使用するとシステムが不安定になり、より高度なプロトコルがないと、すべてのサブスクライバーに確実に配信できない場合があります。 交換されるメッセージの数、および達成したいパフォーマンスパラメータに応じて、RabbitMQ、PubNub、Pusher、CloudAMQP、IronMQ、または他の多くの代替サービスの使用を検討できます。 これらの代替手段は追加の機能を提供し、より複雑なシステムではWisperよりも安定しています。 ただし、実装するには追加の作業も必要です。 メッセージブローカーがどのように機能するかについて詳しくは、こちらをご覧ください。

無限のイベントループ

システムがイベントによって完全に駆動される場合は、イベントループが発生しないように特に注意する必要があります。 これらのループは、コードで発生する可能性のある無限ループとまったく同じです。 ただし、事前に検出するのは難しく、システムを停止させる可能性があります。 システム全体で公開およびサブスクライブされているイベントが多数ある場合、通知なしに存在する可能性があります。

Railsチュートリアルの結論

パブリッシュ/サブスクライブパターンは、Railsのすべての問題とコードの臭いに対する特効薬ではありませんが、さまざまなシステムコンポーネントを分離し、保守性、読み取り性、拡張性を高めるのに役立つ非常に優れたデザインパターンです。

単一責任サービスオブジェクト(SRSO)と組み合わせると、pub-subは、ビジネスロジックをカプセル化し、さまざまなビジネス上の懸念がモデルやコントローラーに忍び寄るのを防ぐのにも役立ちます。

このパターンを使用した後のパフォーマンスの向上は、主に使用されている基盤となるツールに依存しますが、パフォーマンスの向上は場合によっては大幅に向上する可能性があり、ほとんどの場合、パフォーマンスを損なうことはありません。

ただし、緩い結合の大きな力には、緩く結合されたコンポーネントの保守とリファクタリングの大きな責任が伴うため、pub-subパターンの使用は慎重に検討および計画する必要があります。

イベントは簡単に制御不能になる可能性があるため、単純なpub / subライブラリでは、メッセージブローカーの安定性が保証されない場合があります。

そして最後に、手遅れになるまで見過ごされてしまう無限のイベントループを導入する危険性があります。


私はこのパターンを1年近く使用していますが、このパターンなしでコードを書くことは想像しがたいことです。 私にとって、バックグラウンドジョブ、サービスオブジェクト、懸念事項、コントローラー、モデルがすべて互いにクリーンに通信し、魅力のように連携するのは接着剤です。

このコードのレビューから私が学んだことと同じくらい多くのことを学び、パブリッシュ/サブスクライブパターンにRailsアプリケーションを素晴らしいものにする機会を与えることに刺激を受けたことを願っています。

最後に、Wisperを実装する彼の素晴らしい仕事をしてくれた@krisleechに大いに感謝します。