作者都是各自领域经过审查的专家,并撰写他们有经验的主题. 我们所有的内容都经过同行评审,并由同一领域的Toptal专家验证.
艾哈迈德·阿卜杜勒·哈利姆的头像

Ahmed AbdelHalim

Ahmed是一名后端(API)开发人员,他喜欢构建有用且有趣的工具. 他还拥有网页开发经验.

工作经验

10

Share

The 发布-订阅模式 (或简称pub/sub) 是一种Ruby on Rails消息传递模式,消息的发送者(发布者), 不要将消息编程为直接发送给特定的接收者(订阅者). Instead, 程序员“发布”消息(事件), 在不知道可能有订户的情况下.

Similarly, 订阅者表示对一个或多个事件感兴趣, 只接收你感兴趣的信息, 没有任何出版商的任何知识.

要做到这一点, an intermediary, 称为“消息代理”或“事件总线”, 接收已发布的消息, 然后将它们转发给那些注册接收它们的订阅者.

In other words, Pub-sub是一种用于在不同系统组件之间进行消息通信的模式,这些组件之间无需了解彼此的身份.

在本rails教程中,发布-订阅设计模式如下图所示.

这种设计模式并不新鲜,但不常被 Rails developers. 有许多工具可以帮助您将此设计模式合并到代码库中,例如:

所有这些工具都有不同的底层发布-订阅实现, 但是它们都为Rails应用程序提供了相同的主要优势.

Pub-Sub实现的优势

减少模型/控制器膨胀

这是一种常见的做法, 但这不是最佳实践, 在Rails应用程序中使用一些胖模型或控制器.

发布/订阅模式可以 easily help 分解胖模型或控制器.

Fewer Callbacks

Having a lot of 交织在一起的回调 模型之间是一个众所周知的已知 code smell它一点一点地将模型紧密地耦合在一起,使它们更难维护或扩展.

For example a Post 模型可能如下所示:

# app /模型/职位.rb
class Post
  # ...
  字段:内容,类型:字符串
  # ...

  After_create:create_feed,:notify_follower
  # ...

  def create_feed
    Feed.create!(self)
  end

  def notify_followers
    用户:NotifyFollowers.call(self)
  end
end

And the Post 控制器可能看起来像下面这样:

# app / controllers / api / v1 / posts_controller.rb
class Api::V1::PostsController < Api::V1::ApiController
  # ...
  def create
    @post = current_user.posts.构建(post_params)
    if @post.save
      render_created (@post)
    else 
      render_unprocessable_entity (@post.errors)
    end
  end
  # ...
end

如你所见, Post 模型具有回调函数,这些回调函数将模型与 Feed model and the 用户:NotifyFollowers 服务或关注. 通过使用任何发布/订阅模式, 前面的代码可以重构为如下所示, 它使用Wisper:

# app /模型/职位.rb
class Post
  # ...
  字段:内容,类型:字符串
  # ...
  模型中没有回调!
end

Publishers 使用可能需要的事件有效负载对象发布事件.

# app / controllers / api / v1 / posts_controller.rb
#对应上图中的发布者
class Api::V1::PostsController < Api::V1::ApiController

  包括耳语者:出版商
  # ...
  def create
    @post = current_user.posts.构建(post_params)
    if @post.save
      为任何感兴趣的听众发布关于帖子创建的事件
      发布(:post_create @post)
      render_created (@post)
    else 
      为任何感兴趣的监听器发布关于post错误的事件
      发布(:post_errors @post)
      render_unprocessable_entity (@post.errors)
    end
  end
  # ...
end

Subscribers 只订阅它们希望响应的事件.

# / feed_listener app /侦听器.rb
类FeedListener
  def post_create (post)
    Feed.create!(post)
  end
end
# / user_listener app /侦听器.rb
类UserListener
  def post_create (post)
    用户:NotifyFollowers.call(self)
  end
end

Event Bus 在系统中注册不同的订阅者.

#配置/初始化/耳语者.rb

Wisper.订阅(FeedListener.new)
Wisper.订阅(UserListener.new)

在本例中,发布-订阅模式完全消除了 Post 模型,并帮助模型相互独立工作,对彼此的了解最少, 确保松耦合. 将行为扩展为其他操作只是与所需事件挂钩的问题.

单一责任原则(SRP)

The 单一责任原则 是否真的有助于维护干净的代码库. 坚持这样做的问题是,有时类的职责并不像它应该的那样清晰. 这在mvc(如Rails)中尤其常见。.

Models 应该处理持久性、关联,而不是其他.

Controllers 应该处理用户请求,并作为业务逻辑的包装器(服务对象).

Service Objects 应该封装业务逻辑的职责之一, 为外部服务提供入口点,或者充当模型关注点的替代方案.

由于它的力量,以减少耦合, 发布-订阅设计模式可以与单一职责服务对象(srso)结合使用,以帮助封装业务逻辑, 并禁止业务逻辑潜入模型或控制器. 这使代码库保持干净、可读、可维护和可扩展.

下面是使用发布/订阅模式和服务对象实现的一些复杂业务逻辑的示例:

Publisher

#应用/服务/金融/ order_review.rb
类金融::OrderReview
  包括耳语者:出版商
  # ...
  def self.call(order)
    if order.approved?
      发布(:order_create,顺序)
    else
      发布(:order_decline,顺序)
    end
  end
  # ...

Subscribers

# / client_listener app /侦听器.rb
类ClientListener
  def order_create(顺序)
    #可以使用不同的服务对象实现事务
    Client::Charge.call(order)
    库存:UpdateStock.call(order)
  end

  def order_decline(顺序)
    客户::NotifyDeclinedOrder(顺序)
  end
end

通过使用发布-订阅模式,代码库几乎自动地组织到srso中. Moreover, 实现复杂工作流的代码很容易围绕事件组织起来, 不牺牲可读性的前提下, 可维护性或可伸缩性.

Testing

通过分解胖模型和控制器, 有很多srso, 代码库的测试变得非常困难, 更简单的过程. 在集成测试和模块间通信方面尤其如此. 测试应该简单地确保事件被正确地发布和接收.

Wisper has a testing gem 它添加了RSpec匹配器来简化不同组件的测试.

在前两个例子中(Post example and Order 例),测试应包括以下内容:

Publishers

#规范/服务/金融/ order_review.rb
描述财务:OrderReview做
  它'publish:order_create' do
    @order = Fabricate(:订单,批准:true)
    {Financial::OrderReview.call(@order) }.广播(order_create):
  end

  它'publish:order_decline' do
    @order = Fabricate(:订单,批准:false)
    {Financial::OrderReview.call(@order) }.广播(order_decline):
  end
end

Subscribers

#规范/听众/ feed_listener_spec.rb
描述FeedListener
  它'接收:post_create事件在PostController#create'做
    期望(FeedListner).接收(post_create):.with(Post.last)
    post' /post', {content: '一些帖子内容'},request_headers
  end
end

However, 当发布者是控制器时,测试发布的事件有一些限制.

如果你想多付出一点, 对有效负载进行测试将有助于维护更好的代码库.

如您所见,发布-订阅设计模式测试非常简单. 这只是确保正确发布和接收不同事件的问题.

Performance

This is more of a possible advantage. 发布-订阅设计模式本身对代码性能没有主要的内在影响. However, 与您在代码中使用的任何工具一样, 实现发布/订阅的工具对性能有很大的影响. 有时它可能是一个坏的影响,但有时它可能是非常好的.

首先,一个坏影响的例子: Redis 是高级键值缓存和存储吗. 它通常被称为数据结构服务器.这个流行的工具支持发布/订阅模式,并且非常稳定. However, 如果它在远程服务器上使用(不是部署Rails应用程序的同一台服务器), 由于网络开销,它将导致巨大的性能损失.

另一方面,Wisper有各种用于异步事件处理的适配器,比如 wisper-celluloid, wisper-sidekiq and wisper-activejob. 这些工具支持异步事件和线程执行. 如果应用得当,可以极大地提高应用程序的性能.

The Bottom Line

如果你的目标是提高性能,pub/sub模式可以帮助你达到这个目标. 但是,即使您没有发现使用这种Rails设计模式可以提高性能, 它仍然有助于保持代码的组织性并使其更易于维护. After all, 谁会担心无法维护的代码的性能呢, 或者这一开始就行不通?

发布-订阅实现的缺点

与所有事物一样,发布-订阅模式也有一些可能的缺点.

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

pub/sub模式最大的优点也是它最大的缺点. 发布的数据(事件有效负载)的结构必须得到很好的定义, 很快就变得相当不灵活了. 以修改已发布有效负载的数据结构, 有必要了解所有订户的情况, 也可以修改它们, 或者确保修改与旧版本兼容. 这使得重构Publisher代码变得更加困难.

如果你想避免这种情况,你就必须在定义发布者的有效载荷时格外小心. Of course, 如果你有一个很棒的测试套件, 这就像前面提到的那样测试有效载荷, 在更改发布者的有效负载或事件名称后,您不必担心系统会崩溃.

消息总线稳定性

发布者不知道订阅者的状态,反之亦然. 使用简单的发布/订阅工具, 可能无法确保消息传递总线本身的稳定性, 并确保所有发布的消息都被正确地排队和传递.

当使用简单工具时,正在交换的消息数量的增加导致系统不稳定, 而且,如果没有一些更复杂的协议,可能无法确保向所有订阅者提供服务. 取决于正在交换的消息的数量, 以及你想要达到的性能参数, 您可以考虑使用以下服务 RabbitMQ, PubNub, Pusher, CloudAMQP, IronMQ 或者很多其他的选择. 这些替代方案提供了额外的功能, 并且在更复杂的系统中比Wisper更稳定. 然而,它们也需要一些额外的工作来实现. 您可以阅读更多关于消息代理如何工作的信息 here

无限事件循环

当系统完全由事件驱动时,您应该格外小心不要使用事件循环. 这些循环就像代码中的无限循环一样. 然而,它们很难提前发现,而且它们可能会使您的系统停滞不前. 当有许多事件在整个系统中发布和订阅时,它们可以在不需要您通知的情况下存在.

Rails教程总结

发布-订阅模式并不是解决所有Rails问题和代码异味的灵丹妙药, 但它确实是一个很好的设计模式,可以帮助解耦不同的系统组件, 并使其更易于维护, readable, and scalable.

当与单一职责服务对象(srso)结合使用时, Pub-sub还可以真正帮助封装业务逻辑,防止不同的业务关注点渗透到模型或控制器中.

使用此模式后的性能增益主要取决于所使用的底层工具, 但在某些情况下,性能增益可以得到显著提高, 在大多数情况下,它肯定不会影响性能.

然而,使用“发布-订阅”模式应该仔细研究和规划。 因为随着松耦合的强大功能而来的是巨大的责任 维护和重构 松耦合组件.

因为事态很容易失控, 简单的发布/订阅库可能无法确保消息代理的稳定性.

And finally, 引入无限事件循环是有危险的,直到为时已晚才被注意到.


我使用这种模式已经快一年了, 我很难想象没有它就能写代码. For me, 它是后台工作的粘合剂, service objects, concerns, 控制器和模型都能干净利落地相互沟通,并像魅力一样协同工作.

我希望您从回顾这段代码中学到了和我一样多的东西, 并且您感到受到鼓舞,想要给发布-订阅模式一个机会,让您的Rails应用程序变得很棒.

最后,非常感谢 @krisleech 感谢他出色的执行工作 Wisper.

关于总博客的进一步阅读:

就这一主题咨询作者或专家.
Schedule a call
艾哈迈德·阿卜杜勒·哈利姆的头像
Ahmed AbdelHalim

Located in Berlin, Germany

Member since August 6, 2014

About the author

Ahmed是一名后端(API)开发人员,他喜欢构建有用且有趣的工具. 他还拥有网页开发经验.

Toptal作者都是各自领域经过审查的专家,并撰写他们有经验的主题. 我们所有的内容都经过同行评审,并由同一领域的Toptal专家验证.

工作经验

10

世界级的文章,每周发一次.

订阅意味着同意我们的 privacy policy

世界级的文章,每周发一次.

订阅意味着同意我们的 privacy policy

Toptal开发者

Join the Toptal® community.