TL;DR
这篇文章整理了 Service Object 的一套 Convention,用 PORO 结合 Rails 的功能完成了一个例子,并介绍了一些其他思路。
Why Service Object (Again)?
Service Object 已经不是一个新鲜话题了。从 7 Patterns to Refactor Fat ActiveRecord Models 开始就有不少人尝试照着这些 pattern 从 Rails 项目抽象出各种 object 进行解耦。这些 pattern 也催生了不少 gem ,比如关注 policy 的 Pundit ,关注 form 的 Reform,关注 presenter 的……太多不举例了……
但 Service Object 却很少看到有相关的 gem ,DHH 还跟别人讨论了大半天 service 的话题,看起来每个人对于 Service Object 的理解都有些差别。这是为什么?
我个人的理解是,Service Object 没有一个固定的形态,因为它完全就是业务逻辑的封装。
那讨论还有意义吗?有。因为我们需要它,需要更有效率地使用和讨论它。
Convention over Configuration
说到效率,就不得不提关于 Rails 的核心哲学 Convention over Configuration 。如果你的理解仅仅是用 Convention 省去了配置,那并不是它的全部含义。
Convention 的另一层意义在于,它就是一个最佳实践的表现形式,Rails 本质上是一系列 web 开发中最佳实践的集合体。通过 Convention ,Rails 开发者不仅可以避免为一些琐碎的事情费神,从而去处理真正需要关心的事情。更重要的是,遵循 Convention 的 Rails 项目都长得差不多,这使得 Rails 开发者的经验能够跨项目地重用。而且开发者互相交流起来天生就在一个频道上。We are on the same page !
但真正的项目千差万别,Rails 为我们做的毕竟有限,在没有 Convention 覆盖到的地方,开发者的理解就各有千秋了。Service Object 就是其中最典型的例子。有自己想法的人自然可以不拘泥于形式,但也有不少人在疑惑 “怎么才算 Service Object” 和 “如何更好地实现 Service Object” ?
这篇文章推荐了一些 Service Object 的 Convention ,来自 这篇文章 和 这篇文章。
Service Object & Convention
简单的说,Service Object 是用对象来封装一段操作。通常情况下我们用它封装业务逻辑 。关于什么情况下该使用 Service Object ,7 patterns 里的话我觉得已经总结得很好了。
- 操作逻辑很复杂。
- 操作涉及到多个 model。
- 操作涉及到调用外部服务。
- 操作不是 model 该关注的逻辑(比如定时清理过期数据)。
- 操作涉及到一系列不同的具体实现(比如用 token 认证或者 password 认证),策略模式就是干这个的。
因为和业务逻辑比较接近,Service Object 通常用在 Controller 中,但也可以单独使用(比如在 job , console 或者其他 Service Object 中嵌套使用)。
Service Object 的一些简单的约定:
- 一个 Service Object 只做一件事。
- 每个 Service Object 一个文件,统一放在 app/services 目录下。
- 命名采用动作,比如 SignEstimate ,而不是 EstimateSigner 。