專案到中後期長大時通常會開始整理 fat model,但 code 到底要怎麼重構才會比較好呢?
Refactor 時基本目標
- 解耦
- 易於測試
Service Object
Service Object 是一個純粹的 Ruby Object,又稱為 PORO(Plain Old Ruby Object),簡單且沒有任何繼承關係的純 Ruby 物件,這樣一來也不用擔心繼承了什麼帶來的 side effect
這是很常見的整理手法,通常還會依照不同需求有各種 pattern
- Form object
- Null object
- Query object
而 service 是一個比較中庸的統稱,也就是說,當你發現一個運算邏輯他可能有跨 model 的操作,例如流程控制是屬於商業邏輯的部分,並不是單純操作資料,那麼他就可以被抽出來做 service 而隔離直接繼承 ActiveRecord
抽 service object 幾個要點
- 邏輯複雜
- 牽扯到多 model,無法特別歸類於特定 model
- 會呼叫外部服務,例如發送至 slack
- 與核心邏輯無關,例如定時生成報表
- 可能重複使用
基本約定
- 每個 service object 只做一件事
- Instance 只有 2 個 public API, 通常是
initialize
和perform
(要換成 execute / call 都行) - Class method 只有 1 個 public API
- 回傳值盡可能只有 true / false(定義好就好,盡量單純)
每個團隊可以自行調整這樣的 convention
我比較常用的習慣
class SendSmsService
attr_reader :errors
def initialize(phone, country)
@phone, @country = phone, country
@errors = []
end
def perform
# do something
errors.blank?
end
private
# your private method
end
在其他地方可以這樣呼叫
service = SendSmsService.new("012343455", "zh_TW")
if service.perform
# redirect to somewhere
else
# show error message
flash[:alert] = service.errors.join(", ")
# redirect to somewhere
end
並且能將錯誤訊息從 errors
拿出來,測試也變得更易於測試
所以說, Service Object 沒有一個絕對固定的型態,他基本上就是業務邏輯的抽象封裝。
Library
通常會放在 /lib
之下的檔案,基本上會是能夠跨 project 共用,甚至可以直接包成 gem 給大家使用的。
例如 GoogleApi
、FacebookAuth
之類的
Concern
Concern 是加強版的 mix-in,方便整合不同區塊的程式碼,將一些部分簡單的功能抽出來,可以在多個 model 共用
例如可能有 User / Manager 都要用到 add_role
module RoleManagable
extend ActiveSupport::Concern
def add_role!
# do something
end
def remove_role!
# do something
end
end
然後在 User model 內使用
class User < ApplicationRecord
include RoleManagable
end