Rails 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的使用方法。
顧名思義,Service Object是因為有某些類似的特定功能,像是一個『service』,跟資料庫中的model並無直接關係,因此拉出來獨立成為一個class,在邏輯上會更容易管理。不過在文章中有定義了幾個需要使用service object的情況:
method邏輯極其複雜的時候
跨Model使用,無法特別歸類於特定Model
與外部服務有較多關連 - 並非重要功能
同一種method有許多類似的使用方法
文章中以使用者登入機制為範例,將登入的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,管理比較方便。
在一個Rails app中,直接在controller或model資料夾內建立一個*.rb檔案即可,叫什麼名字不重要,你以為一定要叫service.rb嗎?那只是一個概念,其實我們可以叫各種名字,而在這個範例當中,我把檔名叫做document.rb。重點不在檔案,而是裡面的內容。
像圖中另外再開一個services的資料夾也ok!
接下來我們要在檔案中寫入Document class
class Document
def initialize(the_post)
@post = the_post
end
def to_pdf
@post = ... # 處理pdf檔案
post
end
def to_xlsx
@post = ... # 處理xlsx檔案
post
end
def graph_generation
@post = ... # 繪製線圖
graph
end
end
其中部分省略,重點在於class本身有一個initialize的method是預設在class呼叫時會執行的,這裡我們直接將變數送入這個class中,再進行後續處理。到目前為止service object就算建立完成了!
大家可以直接用Ruby的邏輯來思考,呼叫一個class,就是直接呼叫即可,我們可以在controller action中呼叫:
def show
@post = Post.find(params[:id]).includes(:comment)
respond_to do |format|
format.html {
@graph = Document.new(@post).graph_generation
}
format.json { render :json => @post}
format.pdf { @data = Document.new(@post).to_pdf }
format.xlsx { @data = Document.new(@post).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,因此未來在維護時也很方便辨識。
希望大家都能大致了解~
本文同步刊登於我的部落格:特快車