本文概述
publish-subscribe模式(或简称pub / sub)是Ruby on Rails消息传递模式, 其中消息的发送者(发布者)不对要直接发送到特定接收者(订阅者)的消息进行编程。相反, 程序员在没有任何订阅者任何知识的情况下”发布”消息(事件)。
同样, 订户对一个或多个事件表示兴趣, 并且仅接收感兴趣的消息, 而没有任何发布者的知识。
为此, 称为”消息代理”或”事件总线”的中介会接收已发布的消息, 然后将其转发给注册接收这些消息的订户。
换句话说, pub-sub是一种模式, 用于在不同的系统组件之间传递消息, 而这些组件之间不了解彼此的身份。
这种设计模式并不新鲜, 但Rails开发人员并不常用。有许多工具可帮助将此设计模式整合到你的代码库中, 例如:
- Wisper(我个人更喜欢, 并将进一步讨论)
- EventBus
- EventBGBus(EventBus的分支)
- RabbitMQ
- Redis
所有这些工具都有不同的基础pub-sub实现, 但是它们为Rails应用程序提供了相同的主要优点。
Pub-Sub实施的优势
减少模型/控制器膨胀
在Rails应用程序中包含一些胖模型或控制器是一种常见做法, 但不是最佳实践。
发布/订阅模式可以轻松地帮助分解胖模型或控制器。
回调更少
在模型之间有许多相互交织的回调是一种众所周知的代码味道, 并且一点一点地将模型紧密地耦合在一起, 使它们难以维护或扩展。
例如, 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服务或关注紧密耦合的回调。通过使用任何发布/订阅模式, 可以将先前的代码重构为类似于以下内容的代码, 该代码使用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有一个测试工具, 它添加了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赛璐oid, wisper-sidekiq和wisper-activejob。这些工具支持异步事件和线程执行。如果应用得当, 可以极大地提高应用程序的性能。
底线
如果你的目标是提高性能, 则发布/订阅模式可以帮助你达到目标。但是, 即使你找不到这种Rails设计模式的性能提升, 它仍然可以帮助保持代码井井有条并使其更易于维护。毕竟, 谁能担心无法维护的代码的性能, 或者根本无法解决的代码性能?
Pub-Sub实施的缺点
与所有事物一样, pub-sub模式也有一些可能的缺点。
松散耦合(不灵活的语义耦合)
发布/订阅模式最大的优点就是最大的缺点。发布的数据的结构(事件有效负载)必须得到很好的定义, 并且很快变得僵化。为了修改已发布的有效负载的数据结构, 有必要了解所有订户, 并且也可以对其进行修改, 或者确保所做的修改与旧版本兼容。这使得重构发布者代码变得更加困难。
如果要避免这种情况, 则在定义发布者的有效负载时必须格外谨慎。当然, 如果你有一个出色的测试套件, 可以像前面提到的那样测试有效负载, 则不必担心更改发布者的有效负载或事件名称后系统崩溃的情况。
消息总线稳定性
发布者不了解订阅者的状态, 反之亦然。使用简单的发布/订阅工具, 可能无法确保消息传递总线本身的稳定性, 并且无法确保所有已发布消息都已正确排队和传递。
使用简单工具时, 交换消息的数量增加导致系统不稳定, 如果没有一些更复杂的协议, 可能无法确保向所有用户的传递。根据要交换的消息数量以及要实现的性能参数, 你可能会考虑使用RabbitMQ, PubNub, Pusher, CloudAMQP, IronMQ等服务或许多其他替代方法。这些替代方案提供了额外的功能, 对于更复杂的系统, 它们比Wisper更加稳定。但是, 它们还需要执行一些额外的工作。你可以在此处详细了解消息代理的工作方式
无限事件循环
当系统完全由事件驱动时, 应格外小心, 不要发生事件循环。这些循环就像代码中可能发生的无限循环。但是, 它们很难提前被发现, 并且可以使你的系统停止运行。当在系统中发布和订阅了许多事件时, 它们可能存在而不会通知你。
Rails教程总结
发布-订阅模式并不是解决所有Rails问题和代码气味的灵丹妙药, 但是它是一个非常好的设计模式, 可以帮助分离不同的系统组件, 并使其更易于维护, 可读性和可扩展性。
当与单一职责服务对象(SRSO)结合使用时, pub-sub还可真正帮助封装业务逻辑并防止不同的业务问题潜入模型或控制器中。
使用此模式后, 性能的提高主要取决于所使用的基础工具, 但是在某些情况下, 性能提高可以显着提高, 并且在大多数情况下, 肯定不会损害性能。
但是, 应该仔细研究并计划使用pub-sub模式, 因为松耦合的强大功能带来了维护和重构松耦合组件的巨大责任。
因为事件很容易失控, 所以简单的发布/订阅库可能无法确保消息代理的稳定性。
最后, 存在引入无限事件循环的危险, 直到为时已晚才被注意到。
我使用这种模式已经快一年了, 很难想象没有它就编写代码。对我而言, 正是这种胶水使后台作业, 服务对象, 关注点, 控制器和模型之间彼此清晰地沟通, 并像魅力一样相互协作。
希望你能从回顾这段代码中学到很多东西, 并且希望给Publish-Subscribe模式一个使Rails应用程序变得很棒的机会。
最后, 非常感谢@krisleech为实现Wisper所做的出色工作。
评论前必须登录!
注册