Rails 上的發布-訂閱模式:實現教程

已發表: 2022-03-11

發布-訂閱模式(或簡稱 pub/sub)是一種 Ruby on Rails 消息傳遞模式,其中消息的發送者(發布者)不會將消息編程為直接發送給特定的接收者(訂閱者)。 相反,程序員“發布”消息(事件),而不知道可能有任何訂閱者。

類似地,訂閱者表達對一個或多個事件的興趣,並且只接收感興趣的消息,而不知道任何發布者。

為此,稱為“消息代理”或“事件總線”的中介接收已發布的消息,然後將它們轉發給註冊接收它們的訂閱者。

換句話說,pub-sub 是一種用於在不同系統組件之間傳遞消息的模式,而這些組件不知道彼此的身份。

在本 Rails 教程中,發布-訂閱設計模式在此圖中進行了佈局。

這種設計模式並不新鮮,但 Rails 開發人員並不常用。 有許多工具可以幫助將此設計模式整合到您的代碼庫中,例如:

  • Wisper(我個人更喜歡並將進一步討論)
  • 事件總線
  • EventBGBus(EventBus 的一個分支)
  • 兔MQ
  • 雷迪斯

所有這些工具都有不同的底層 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)時尤其常見。

模型應該處理持久性、關聯性,而不是其他。

控制器應該處理用戶請求並成為業務邏輯(服務對象)的包裝器。

服務對象應封裝業務邏輯的職責之一,為外部服務提供入口點,或充當模型關注點的替代方案。

由於其減少耦合的能力,pub-sub 設計模式可以與單一職責服務對象 (SRSO) 相結合,以幫助封裝業務邏輯,並禁止業務邏輯潛入模型或控制器。 這使代碼庫保持乾淨、可讀、可維護和可擴展。

以下是使用發布/訂閱模式和服務對象實現的一些複雜業務邏輯的示例:

出版商

# 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 有一個測試 gem,它添加了 RSpec 匹配器以簡化不同組件的測試。

在前面的兩個示例( 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 的工具會對性能產生很大影響。 有時它可能會產生不好的影響,但有時它可能會非常好。

首先,一個壞影響的例子:Redis 是一個“高級鍵值緩存和存儲”。 它通常被稱為數據結構服務器。” 這個流行的工具支持 pub/sub 模式並且非常穩定。 但是,如果在遠程服務器上使用(不是部署 Rails 應用程序的同一台服務器),則會由於網絡開銷而導致巨大的性能損失。

另一方面,Wisper 有各種用於異步事件處理的適配器,例如 wisper-celluloid、wisper-sidekiq 和 wisper-activejob。 這些工具支持異步事件和線程執行。 如果應用得當,可以極大地提高應用程序的性能。

底線

如果您的目標是在性能上加倍努力,那麼 pub/sub 模式可以幫助您實現這一目標。 但是,即使您沒有發現這種 Rails 設計模式的性能提升,它仍然有助於保持代碼的組織性並使其更易於維護。 畢竟,誰能擔心無法維護或一開始就不起作用的代碼的性能呢?

Pub-Sub 實施的缺點

與所有事情一樣,pub-sub 模式也有一些可能的缺點。

松耦合(不靈活的語義耦合)

pub/sub 模式最大的優點也是它最大的缺點。 發布的數據(事件有效負載)的結構必須明確定義,並且很快就會變得相當不靈活。 為了修改已發布有效負載的數據結構,有必要了解所有訂閱者,並且要么也修改它們,要么確保修改與舊版本兼容。 這使得 Publisher 代碼的重構變得更加困難。

如果您想避免這種情況,則在定義發布者的有效負載時必須格外小心。 當然,如果您有一個很棒的測試套件,可以像前面提到的那樣測試有效負載,那麼您不必擔心在更改發布者的有效負載或事件名稱後系統會崩潰。

消息總線穩定性

發布者不知道訂閱者的狀態,反之亦然。 使用簡單的發布/訂閱工具,可能無法確保消息總線本身的穩定性,並確保所有發布的消息都正確排隊和傳遞。

當使用簡單的工具時,正在交換的消息數量增加會導致系統不穩定,如果沒有一些更複雜的協議,可能無法確保向所有訂閱者傳遞。 根據正在交換的消息數量以及您想要實現的性能參數,您可能會考慮使用 RabbitMQ、PubNub、Pusher、CloudAMQP、IronMQ 等服務或許多其他替代方案。 這些替代方案提供了額外的功能,並且對於更複雜的系統比 Wisper 更穩定。 但是,它們還需要一些額外的工作來實現。 您可以在此處閱讀有關消息代理如何工作的更多信息

無限事件循環

當系統完全由事件驅動時,您應該格外小心,不要有事件循環。 這些循環就像代碼中可能發生的無限循環一樣。 但是,它們更難提前檢測到,它們可能會使您的系統陷入停頓。 當系統中發布和訂閱了許多事件時,它們可能會在您不通知的情況下存在。

Rails 教程結論

發布-訂閱模式並不是解決所有 Rails 問題和代碼異味的靈丹妙藥,但它是一種非常好的設計模式,有助於解耦不同的系統組件,使其更易於維護、可讀和可擴展。

當與單一職責服務對象 (SRSO) 結合使用時,pub-sub 還可以真正幫助封裝業務邏輯並防止不同的業務關注點蔓延到模型或控制器中。

使用此模式後的性能增益主要取決於所使用的底層工具,但在某些情況下性能增益可以顯著提高,並且在大多數情況下肯定不會損害性能。

但是,應該仔細研究和規劃 pub-sub 模式的使用,因為鬆散耦合的強大功能帶來了維護和重構鬆散耦合組件的重大責任。

因為事件很容易失控,一個簡單的發布/訂閱庫可能無法確保消息代理的穩定性。

最後,存在引入無限事件循環的危險,直到為時已晚才被注意到。


我已經使用這種模式將近一年了,我很難想像沒有它來編寫代碼。 對我來說,它是使後台作業、服務對象、關注點、控制器和模型彼此乾淨地通信並像魅力一樣協同工作的粘合劑。

我希望你和我從查看這段代碼中學到的一樣多,並且你會受到啟發,給 Publish-Subscribe 模式一個機會,讓你的 Rails 應用程序變得很棒。

最後,非常感謝 @krisleech 為實現 Wisper 所做的出色工作。