Rails code 整理系列 - Service Object 初探
Rails在设计上有太多可以自行调整的风格,这也是为什么在高阶开发者之间有许多争议和讨论,例如今年 在RailsConf引爆的『TDD已死』就是一个很好的例子,没有对错,只有风格。
而在Rails结构上也有许多可以调整的地方,例如常听到的『skinny controller、fat model』就是一例,认为所谓controller就是掌管各种『行为』(action),而不去干涉前端(view)的设计,也不去牵扯太多跟数据库(model)有关的计算。 Fat model就是将各种跟计算、储存有关的事情都交给model来处理,就这样的逻辑来分配,对开发者来说比较容易理解。
但照这样逻辑来推算,如果把所有东西都交给model,在逻辑、各种计算不分类的情况下,整个code也会变得相当混乱。 经典文章『7 Patterns to Refactor Fat ActiveRecord Models』便提到利用七种方法让过度拥挤的model可以有喘息的地方 ,解决这个整理上的困扰。 而本篇则分享其中一个Service Object的使用方法。
1. 为何要使用Service Object
顾名思义,Service Object是因为有某些类似的特定功能,像是一个『service』,跟数据库中的model并无直接关系,因此拉出来独立成为一个class,在逻辑上会更容易管理。 不过在文章中有定义了几个需要使用service object的情况:
- method逻辑极其复杂的时候
- 跨Model使用,无法特别归类于特定Model
- 与外部服务有较多关连
- 并非重要功能
- 同一种method有许多类似的使用方法
2. Service Object 使用时机
文章中以用户登入机制为范例,将登入的authenticate功能另外放在UserAuthenticator
这个class当中,非常简单的一个介绍。 但通常在实作中会遇到的情况都复杂许多,我个人遇过的情况是:
实作规格:
- Group、Post、Comment 三个model
- 要能将Post输出成四种格式:html、json、xlsx、pdf
- 在每种格式中要能列出相对应Group和Comment,并依据数量画出线图
这听起来就需要非常多分门别类的功能,但聪明的大家应该不难看出,这些功能几乎都是使用外部服务,包括gem 'jbuilder'、gem 'axlsx'、gem 'prawn'、gem 'gruff'四个都是不需要自己土法炼钢的功能,而是直接使用gem安装以后,就可以将格式输出了。
因此,这些内容都是专门处理『输出』(render)的部份,可以将他们收拢为一个service object,管理比较方便。
3. 如何建立Service Object
在一个Rails app中,直接在controller或model文件夹内建立一个*.rb
档案即可,叫什么名字不重要,你以为一定要叫service.rb
吗? 那只是一个概念,其实我们可以叫各种名字,而在这个范例当中,我把档名叫做document.rb
。 重点不在档案,而是里面的内容。
像图中另外再开一个services的文件夹也ok! 如果没有与controller或model开在同一个目录底下,需要在code中多安插一行require "service"
接下来我们要在档案中写入Document class
class Document
def initialize(post)
@the_post = post
end
def to_pdf
@the_post = ... # 处理pdf档案
post
end
def to_xlsx
@the_post = ... # 处理xlsx档案
post
end
def graph_generation
@the_post = ... # 绘制线图
graph
end
end
其中部分省略,重点在于clas s本身有一个initialize
的method是默认在class呼叫时会执行的,这里我们直接将变量指定给这个class当中的@post变量,再利用其instance variable特性传送给各个method进行后续处理。 到目前为止service object就算建立完成了!
4. 如何使用Service Object
大家可以直接用Ruby的逻辑来思考,呼叫一个class,就是直接呼叫即可,我们可以在controller action中呼叫:
def show
@post = Post.find(params[:id]).includes(:comment)
document = Document.new(@ post)
respond_to do |format|
format.html {
@graph = document.graph_generation
}
format.json { render :json => @post}
format.pdf { @data = document.to_pdf }
format.xlsx { @data = document.to_xlsx }
end
end
简单说明,使用 Service Object时,
1. 呼叫class
在这里使用开头大写的class名称,由于呼叫的方式跟呼叫model相同,因此记得不要取相同的名字。
2. 加上new method
加上new method代表的是执行刚才定义的initialize这个method,由于呼叫service object时一定要先启动,所以这个new method是不可省略的。 接着在后方加上要带入的变量,在这里是带入@post
,会对照到刚才我们在class中定义的the_post
。
3. 再加上要使用的method
变量带入以后,我们就可以再带入其他的method,针对不同输出方式来调整。 由于这边的范例是在controller里面使用instance variable,所以instance variable的部份(前面有加上@
符号的变量,可以在controller之间互相传递)要记得不要取相同的名字,以免重复使用。 如果是在model里面使用service object比较不会有这个困扰。
由于这边是重复呼叫Document
,才会先指定给在document
。 如果只使用一次,也可以只写成一行:@data = Document.new(@post).to_pdf
依照这样的写法,我们就把所有的功能都放到service object当中了,剩下的事情交给前端的view去处理。 优点是controller保持干净简单的特性,其中各项功能都有标注是来自Document这个class的method,因此未来在维护时也很方便辨识。
希望大家都能抓到个概念~