iT邦幫忙

DAY 7
2

Ruby on Rails 花招百出系列 第 8

Rails code 整理系列 - Service Object 初探

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的使用方法。

1. 為何要使用Service Object

顧名思義,Service Object是因為有某些類似的特定功能,像是一個『service』,跟資料庫中的model並無直接關係,因此拉出來獨立成為一個class,在邏輯上會更容易管理。不過在文章中有定義了幾個需要使用service object的情況:

  • method邏輯極其複雜的時候

  • 跨Model使用,無法特別歸類於特定Model

  • 與外部服務有較多關連 - 並非重要功能

  • 同一種method有許多類似的使用方法

2. Service Object 使用時機

文章中以使用者登入機制為範例,將登入的authenticate功能另外放在UserAuthenticator這個class當中,非常簡單的一個介紹。但通常在實作中會遇到的情況都複雜許多,我個人遇過的情況是:

實作規格:

  1. Group < Post < Comment 三個model

  2. 要能將Post輸出成四種格式:html、json、xlsx、pdf

  3. 在每種格式中要能列出相對應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!

接下來我們要在檔案中寫入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就算建立完成了!

4. 如何使用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,因此未來在維護時也很方便辨識。

希望大家都能大致了解~

延伸閱讀

BrewHouse

RailsCast(付費)

本文同步刊登於我的部落格:特快車


上一篇
Ruby code 整理系列 if...else
下一篇
Ruby code整理系列 ||= 的使用
系列文
Ruby on Rails 花招百出32

尚未有邦友留言

立即登入留言