Nic Lin's Blog

喜歡在地上滾的工程師

[Rails] Service / Library / Concern 的差異

專案到中後期長大時通常會開始整理 fat model,但 code 到底要怎麼重構才會比較好呢?

Refactor 時基本目標

  1. 解耦
  2. 易於測試

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 幾個要點

  1. 邏輯複雜
  2. 牽扯到多 model,無法特別歸類於特定 model
  3. 會呼叫外部服務,例如發送至 slack
  4. 與核心邏輯無關,例如定時生成報表
  5. 可能重複使用

基本約定

  • 每個 service object 只做一件事
  • Instance 只有 2 個 public API, 通常是 initializeperform(要換成 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 給大家使用的。

例如 GoogleApiFacebookAuth 之類的

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

參考來源

comments powered by Disqus