Rabel 源码探秘:CreateTopic 业务类与 Fanli 事件驱动的设计之道
2026/8/21 21:22:03 网站建设 项目流程

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(...)。这里的.withFanli::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;当「创建评论」事件发生时,执行NotifyMentionedUsersTouchCommentable

这就是事件驱动的精髓:

  1. 业务类只负责「把数据存进数据库」这一件事;
  2. 事件总线负责在合适时机广播:create_topic:create_comment等事件;
  3. 监听器(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 事件驱动,可以提炼出三点对新手最有价值的设计经验:

  1. 把业务从 Controller 中搬出来:每个核心动作对应一个类,类名即业务名,代码即文档;
  2. 用事件解耦副作用:主流程只做「写数据」,通知、计数、缓存刷新全部交给事件监听器,新增需求零侵入;
  3. 统一入口约定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),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询