作者都是各自领域经过审查的专家,并撰写他们有经验的主题. 我们所有的内容都经过同行评审,并由同一领域的Toptal专家验证.
Ahmed是一名后端(API)开发人员,他喜欢构建有用且有趣的工具. 他还拥有网页开发经验.
10
The 发布-订阅模式 (或简称pub/sub) 是一种Ruby on Rails消息传递模式,消息的发送者(发布者), 不要将消息编程为直接发送给特定的接收者(订阅者). Instead, 程序员“发布”消息(事件), 在不知道可能有订户的情况下.
Similarly, 订阅者表示对一个或多个事件感兴趣, 只接收你感兴趣的信息, 没有任何出版商的任何知识.
要做到这一点, an intermediary, 称为“消息代理”或“事件总线”, 接收已发布的消息, 然后将它们转发给那些注册接收它们的订阅者.
In other words, Pub-sub是一种用于在不同系统组件之间进行消息通信的模式,这些组件之间无需了解彼此的身份.
这种设计模式并不新鲜,但不常被 Rails developers. 有许多工具可以帮助您将此设计模式合并到代码库中,例如:
所有这些工具都有不同的底层发布-订阅实现, 但是它们都为Rails应用程序提供了相同的主要优势.
这是一种常见的做法, 但这不是最佳实践, 在Rails应用程序中使用一些胖模型或控制器.
发布/订阅模式可以 easily help 分解胖模型或控制器.
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
模型,并帮助模型相互独立工作,对彼此的了解最少, 确保松耦合. 将行为扩展为其他操作只是与所需事件挂钩的问题.
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, 实现复杂工作流的代码很容易围绕事件组织起来, 不牺牲可读性的前提下, 可维护性或可伸缩性.
通过分解胖模型和控制器, 有很多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, 当发布者是控制器时,测试发布的事件有一些限制.
如果你想多付出一点, 对有效负载进行测试将有助于维护更好的代码库.
如您所见,发布-订阅设计模式测试非常简单. 这只是确保正确发布和接收不同事件的问题.
This is more of a possible advantage. 发布-订阅设计模式本身对代码性能没有主要的内在影响. However, 与您在代码中使用的任何工具一样, 实现发布/订阅的工具对性能有很大的影响. 有时它可能是一个坏的影响,但有时它可能是非常好的.
首先,一个坏影响的例子: Redis 是高级键值缓存和存储吗. 它通常被称为数据结构服务器.这个流行的工具支持发布/订阅模式,并且非常稳定. However, 如果它在远程服务器上使用(不是部署Rails应用程序的同一台服务器), 由于网络开销,它将导致巨大的性能损失.
另一方面,Wisper有各种用于异步事件处理的适配器,比如 wisper-celluloid, wisper-sidekiq and wisper-activejob. 这些工具支持异步事件和线程执行. 如果应用得当,可以极大地提高应用程序的性能.
如果你的目标是提高性能,pub/sub模式可以帮助你达到这个目标. 但是,即使您没有发现使用这种Rails设计模式可以提高性能, 它仍然有助于保持代码的组织性并使其更易于维护. After all, 谁会担心无法维护的代码的性能呢, 或者这一开始就行不通?
与所有事物一样,发布-订阅模式也有一些可能的缺点.
pub/sub模式最大的优点也是它最大的缺点. 发布的数据(事件有效负载)的结构必须得到很好的定义, 很快就变得相当不灵活了. 以修改已发布有效负载的数据结构, 有必要了解所有订户的情况, 也可以修改它们, 或者确保修改与旧版本兼容. 这使得重构Publisher代码变得更加困难.
如果你想避免这种情况,你就必须在定义发布者的有效载荷时格外小心. Of course, 如果你有一个很棒的测试套件, 这就像前面提到的那样测试有效载荷, 在更改发布者的有效负载或事件名称后,您不必担心系统会崩溃.
发布者不知道订阅者的状态,反之亦然. 使用简单的发布/订阅工具, 可能无法确保消息传递总线本身的稳定性, 并确保所有发布的消息都被正确地排队和传递.
当使用简单工具时,正在交换的消息数量的增加导致系统不稳定, 而且,如果没有一些更复杂的协议,可能无法确保向所有订阅者提供服务. 取决于正在交换的消息的数量, 以及你想要达到的性能参数, 您可以考虑使用以下服务 RabbitMQ, PubNub, Pusher, CloudAMQP, IronMQ 或者很多其他的选择. 这些替代方案提供了额外的功能, 并且在更复杂的系统中比Wisper更稳定. 然而,它们也需要一些额外的工作来实现. 您可以阅读更多关于消息代理如何工作的信息 here
当系统完全由事件驱动时,您应该格外小心不要使用事件循环. 这些循环就像代码中的无限循环一样. 然而,它们很难提前发现,而且它们可能会使您的系统停滞不前. 当有许多事件在整个系统中发布和订阅时,它们可以在不需要您通知的情况下存在.
发布-订阅模式并不是解决所有Rails问题和代码异味的灵丹妙药, 但它确实是一个很好的设计模式,可以帮助解耦不同的系统组件, 并使其更易于维护, readable, and scalable.
当与单一职责服务对象(srso)结合使用时, Pub-sub还可以真正帮助封装业务逻辑,防止不同的业务关注点渗透到模型或控制器中.
使用此模式后的性能增益主要取决于所使用的底层工具, 但在某些情况下,性能增益可以得到显著提高, 在大多数情况下,它肯定不会影响性能.
然而,使用“发布-订阅”模式应该仔细研究和规划。 因为随着松耦合的强大功能而来的是巨大的责任 维护和重构 松耦合组件.
因为事态很容易失控, 简单的发布/订阅库可能无法确保消息代理的稳定性.
And finally, 引入无限事件循环是有危险的,直到为时已晚才被注意到.
我使用这种模式已经快一年了, 我很难想象没有它就能写代码. For me, 它是后台工作的粘合剂, service objects, concerns, 控制器和模型都能干净利落地相互沟通,并像魅力一样协同工作.
我希望您从回顾这段代码中学到了和我一样多的东西, 并且您感到受到鼓舞,想要给发布-订阅模式一个机会,让您的Rails应用程序变得很棒.
最后,非常感谢 @krisleech 感谢他出色的执行工作 Wisper.
Ahmed是一名后端(API)开发人员,他喜欢构建有用且有趣的工具. 他还拥有网页开发经验.
10
世界级的文章,每周发一次.
世界级的文章,每周发一次.
Join the Toptal® community.