8分钟阅读

Rails上发布 - 订阅模式:实施教程

艾哈迈德是一个后端(API)开发人员,他喜欢建立有用和有趣的工具。他还拥有Web开发人员的经验。

发布 - 订阅模式 (或PUB / SUB,短暂) 是Ruby上的Rails消息传递模式,消息的发件人(发布者),请勿将要直接发送到特定接收器(订阅者)的消息。相反,程序员“发布”消息(事件),而不会有任何订阅者的知识。

同样,订阅者在一个或多个事件中表达兴趣,并且只接收感兴趣的消息,而不知道任何发布者。

为实现此目的,称为“消息代理”或“事件总线”的中间人接收已发布的消息,然后将它们转发到注册以接收它们的那些订阅者。

换句话说,Pub-sub是用于在不同系统组件之间传送消息的模式,而不知道彼此的身份的任何内容。

在该轨道教程中,在该图中布置出版资本设计模式。

这种设计模式并不新鲜,但它不常用 Rails开发人员。有许多工具可以帮助将此设计模式纳入代码库,例如:

所有这些工具都有不同的底层PUB-亚实施,但它们都为Rails应用提供了相同的主要优势。

PUB-SUB实施的优势

减少模型/控制器膨胀

这是一个常见的做法,但不是最好的做法,在铁路应用中拥有一些胖模型或控制器。

酒吧/子模式可以 容易地 帮助 分解脂肪模型或控制器.

较少的回调

有很多 交织的回调 在模型之间是一个众所周知的知名 代码闻 并且逐点钟将模型紧紧地耦合在一起,使它们更难维持或扩展。

For example a Post model could look like the following:

# 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

And the Post controller might look something like the following:

# 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

As you can see, the Post model has callbacks that tightly couple the model to both the Feed model and the User::NotifyFollowers service or concern. By using any pub/sub pattern, the previous code could be re-factored to be something like the following, which uses 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)

In this example, the pub-sub pattern completely eliminated callbacks in the Post model and helped the models to work independently from each other with minimum knowledge about each other, ensuring loose coupling. Expanding the behavior to additional actions is just a matter of hooking to the desired event.

单一责任原则(SRP)

单一责任原则 真的有助于维护干净的代码库。坚持下面的问题是,班级的责任不如它所示。当涉及到MVCS(如轨道)时,这尤其常见。

楷模 应该处理持久性,协会,也不是其他的。

控制器 应该处理用户请求,并在业务逻辑(服务对象)周围是包装器。

服务对象 应该封装业务逻辑的职责之一, 提供外部服务的入口点,或者作为模型问题的替代品。

由于其降低耦合的力量,Pub-Sub设计模式可以与单责任服务对象(SRSO)组合,以帮助封装业务逻辑,并禁止从Creeping进入模型或控制器的业务逻辑。这使得代码基于干净,可读,可维护性和可扩展性。

以下是使用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

通过使用Publish-Subscribe模式,代码库几乎自动组织到SRSO中。此外,在事件周围容易组织复杂工作流的实现代码,而不会牺牲可读性,可维护性或可扩展性。

测试

通过分解胖模型和控制器,并具有很多SRSO,对代码基础的测试变得更加简单。尤其是谈到集成测试和模块间通信时的情况。测试应该只需确保事件已正确发布和收到。

Wisper. 有A. 测试宝石 这会添加RSPEC匹配器以简化对不同组件的测试。

In the previous two examples (Post example and Order example), testing should include the following:

出版商

# 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. 是一个“高级键值缓存和存储。它通常被称为数据结构服务器。“这种流行的工具支持PUB / SUB模式,非常稳定。但是,如果它在远程服务器上使用(不同一服务器,Rails应用程序部署),则它将导致由于网络开销导致的巨大性能损失。

另一方面,Wisper拥有各种适配器,适用于异步事件处理,如 Wisper. -Celluloid., Wisper. -sidekiq. Wisper. -ActiveJob.。这些工具支持异步事件和线程执行。 如果适当申请,可以大大提高应用程序的性能。

底线

如果您的表现额外互联网,PUB / Sub模式可以帮助您到达它。但即使您没有找到具有此轨道设计模式的性能提升,它仍然有助于保持代码组织并使其更加可维护。毕竟,谁可以担心无法维护的代码的表现,或者这不在第一次工作?

PUB子实施的缺点

与所有事情一样,PUB子模式也存在一些可能的缺点。

松散耦合(不灵活的语义耦合)

最大的酒吧/子模式的优势也是最大的弱点。发布的数据(事件有效载荷)的结构必须定义很好,并且很快变得相当不灵活。为了修改已发布的有效载荷的数据结构,有必要了解所有订阅者,也需要修改它们,或者确保修改与旧版本兼容。这使得出版商代码重构更加困难。

如果您想避免这种情况,您必须在定义发布商的有效载荷时更加谨慎。当然,如果您有一个伟大的测试套件,那么测试有效载荷以及先前提到的,如果您不必在更改发布者的有效负载或事件名称后担心系统下降。

消息传递总线稳定性

出版商没有了解订户的状态,反之亦然。使用简单的Pub / sub工具,可能无法确保消息传递总线本身的稳定性,并确保所有已发布的消息都正确排队和交付。

在使用简单工具时,越来越多的消息越来越多的消息导致系统中的不稳定性,并且可能无法确保在没有一些更复杂的协议的情况下向所有订户提供交付。根据正在交换多少条消息,以及您想要实现的性能参数,您可能会考虑使用这样的服务 rabbitmq. , Pubnub. , 推动 , Cloudamqp. , 或许多其他替代品。这些替代方案提供额外的功能,并且比更复杂的系统更稳定。但是,他们还需要一些额外的工作来实施。您可以了解更多关于消息经纪人如何工作的信息 这里

无限事件环

当系统完全由事件驱动时,您应该更加谨慎,不要有活动环。这些循环就像在代码中可能发生的无限循环一样。但是,它们更难以检测到时间,并且他们可以将系统带到静止状态。如果在系统上发布和订阅许多活动,则可以在没有通知的情况下存在。

Rails教程结论

发布订阅模式不是一个银弹,用于所有轨道问题和代码气味,但它是一个非常好的设计模式,有助于解耦不同的系统组件,并使其更加可维护,可读和可扩展。

当与单责任服务对象(SRSOS)结合时,PUB-SUB也可以帮助封装业务逻辑并防止从爬行到模型或控制器的不同业务问题。

使用此模式后的性能增益主要取决于正在使用的底层工具上,但在某些情况下,可以显着提高性能增益,并且在大多数情况下,它肯定不会损害性能。

但是,应仔细研究和计划使用PUB-子模式, 因为随着松散耦合的大力量是责任 维护和重构 松散耦合的组件。

由于事件可以很容易地扩大到控制,因此简单的PUB /子库可能无法确保消息代理的稳定性。

最后,存在引入无情的事件环,直到为时已晚。


我现在一直在使用这种模式,差不多一年,我很难想象没有它的写作代码。对我来说,它是制作背景职位,服务对象,控制器和模型的胶水彼此干净地沟通,并像魅力一样工作。

我希望您从审核此代码中获得的那样了解,并且您感觉到发布的订阅模式有机会使您的Rails应用程序令人敬畏。

最后,一个巨大的谢谢 @Krisleech. 为了他实现的令人敬畏的工作 Wisper. .