Rabel 源码探秘:CreateTopic 业务类与 Fanli 事件驱动的设计之道
【免费下载链接】rabelAn open-source web forum built on the Ruby on Rails framework.项目地址: https://gitcode.com/gh_mirrors/ra/rabel
Rabel 是一个基于 Ruby on Rails 框架的开源 Web 论坛系统,代码干净、结构清晰,非常适合想学习 Rails 工程化设计的开发者。本文将从Rabel 源码中的app/business目录出发,拆解CreateTopic 业务类的写法,并揭秘其背后的Fanli 事件驱动机制,看看一个发帖动作是如何做到「业务瘦身、关注点分离」的。
为什么 Rabel 值得读源码?📚
很多论坛项目的业务逻辑都堆在 Controller 里,一个create方法动辄几十行:校验、建模型、发通知、更新计数……代码越写越长,越改越乱。
Rabel 的做法完全不同:它把「发帖」「回帖」「打卡」这些核心动作,全部抽成独立的业务类(Service Object),统一放在 app/business/ 目录下:
| 文件 | 职责 |
|---|---|
| create_topic.rb | 创建话题(发帖) |
| create_comment.rb | 创建评论(回帖) |
| destroy_comment.rb | 删除评论 |
| check_in.rb | 每日签到与连续签到统计 |
| notify_mentioned_users.rb | @ 提及用户并发送通知 |
| touch_commentable.rb | 回帖后刷新话题的活跃时间与最后回复人 |
这些业务类的共同点是:都继承自Fanli::Base,也就是标题里提到的事件驱动核心。
主角登场:CreateTopic 业务类源码解析 🔍
先来看本次的主角——create_topic.rb:
class CreateTopic < Fanli::Base def initialize(user, channel, params) @user = user @channel = channel @params = params end def perform @topic = @channel.topics.new(@params) @topic.user = @user return @topic if @topic.save end end别看它只有十几行,逻辑其实很完整:
initialize接收依赖:把当前用户、所属频道、表单参数三个对象注入进来,业务类不自己去查数据库,依赖关系一目了然;perform只做一件事:把话题挂到频道下、绑定作者、保存并返回结果;- 返回值即结果:保存成功返回
@topic,失败则返回nil,调用方拿到结果就知道成败。
这种「构造函数注入依赖 + 单一perform方法」的写法,就是经典的服务对象模式(Service Object),也是 Rabel 源码中最值得新手模仿的设计之一。
瘦控制器:一行代码搞定发帖流程 🏗️
业务类写好后,Controller 就变得异常清爽。看 topics_controller.rb 里的create动作:
def create if CreateTopic.with(current_user, @channel, topic_params) redirect_to t_path(@topic.id) else render :new end end整个发帖流程被压缩成了一行调用:CreateTopic.with(...)。这里的.with是Fanli::Base提供的类方法——它负责实例化业务类、执行perform、然后在事件总线上广播事件。成功就跳转到话题页,失败就重新渲染表单页,Controller 不再关心任何业务细节。
再看 comments_controller.rb 的回帖动作,同样是这个套路:
@comment = CreateComment.with(current_user, @commentable, comment_params)一致的调用风格让整个项目的 Controller 保持了极高的可读性,这就是「胖业务、瘦控制器」的实践范本。
解密 Fanli 事件驱动机制 ⚙️
关键问题来了:Fanli::Base的.with到底做了什么?它和事件驱动又有什么关系?
答案藏在 Gemfile 里引用的fanli gem(0.4.0)和 config/initializers/business.rb 中:
Fanli.on(:create_topic, NotifyMentionedUsers) Fanli.on(:create_comment, NotifyMentionedUsers, TouchCommentable) Fanli.on(:destroy_comment, TouchCommentable)这套 API 的意思非常直白:当「创建话题」事件发生时,自动执行NotifyMentionedUsers;当「创建评论」事件发生时,执行NotifyMentionedUsers和TouchCommentable。
这就是事件驱动的精髓:
- 业务类只负责「把数据存进数据库」这一件事;
- 事件总线负责在合适时机广播
:create_topic、:create_comment等事件; - 监听器(Listener)按需订阅事件,各自完成「发通知」「刷新时间戳」等副作用。
于是「发帖」和「通知 @ 用户」彻底解耦——以后想加「发帖后发邮件」「发帖后推送站内信」,只需新增一个监听器并订阅事件,完全不用改动 CreateTopic 本体。
小提示:Rabel 的 business.rb 里这些订阅目前被注释掉了,但事件驱动的「骨架」已经完整保留,读者可以自行打开开关感受效果。
事件订阅实战:@提醒与回帖触达 💬
1. 自动识别 @ 提及的用户
notify_mentioned_users.rb 里的on_create_topic方法,会在发帖事件后自动扫描标题和正文:
- 用正则
Notifiable::MENTION_REGEXP匹配所有@昵称; - 去掉发帖人自己,避免「自己 @ 自己」;
- 按昵称查到真实用户,逐个写入
Notification通知记录。
回帖场景的on_create_comment更进一步:既会通知楼主「有人回复了你」,也会识别评论中的 @ 提及并单独通知,还细心地排除了楼主本人。一套代码,两种通知,全靠事件驱动。
2. 回帖后自动刷新话题热度
touch_commentable.rb 则负责「保持列表热度」:新评论出现时,更新话题的last_replied_by(最后回复人)和last_replied_at(最后回复时间);删除评论时,则回退到上一条评论的信息,甚至清空为初始值。这也解释了论坛首页为什么总能把「刚被回复的帖子」顶到最前面。
不止发帖:其他业务类同样精彩 🌟
事件驱动的写法在整个业务层一以贯之,比如 check_in.rb 的每日签到逻辑:
- 通过
CheckedInAtQuery查询今天是否已签到,已签到则直接返回; - 生成今天的签到记录并保存;
- 若昨天也签到了,则把「连续签到天数」加一。
可以看到,连签到这种轻量功能也被封装成了独立业务类,配合 checkins_controller.rb 里的CheckIn.with(current_user)一行调用,整洁得让人舒适。
从 Rabel 源码中学到的三条设计经验 ✨
读完 CreateTopic 业务类与 Fanli 事件驱动,可以提炼出三点对新手最有价值的设计经验:
- 把业务从 Controller 中搬出来:每个核心动作对应一个类,类名即业务名,代码即文档;
- 用事件解耦副作用:主流程只做「写数据」,通知、计数、缓存刷新全部交给事件监听器,新增需求零侵入;
- 统一入口约定:
initialize注入依赖、perform执行逻辑、.with统一调度,全项目一套规范,团队协作成本极低。
如果你也想让自己的 Rails 项目从「堆代码」进化为「设计良好」,不妨把 Rabel 的业务层当作一份活教材——先读懂 create_topic.rb 这十几行,再顺着 business.rb 的事件订阅把整个业务层串起来,你会发现:好代码,真的可以像读故事一样轻松。🚀
【免费下载链接】rabelAn open-source web forum built on the Ruby on Rails framework.项目地址: https://gitcode.com/gh_mirrors/ra/rabel
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考