職責劃分 - 重新思考 Rails 架構
透過釐清脈絡、邊界後,我們大致上就能對整個系統的全貌有一定程度的理解。接下來會需要根據當下系統的狀況,釐清我們需要有哪些類型的物件,以及擔任怎樣的職責,來確保單一職責(Single-responsibility principle)
職責
Rails 作為一個 MVC 框架,對於物件的職責還是相當容易區分。然而這只限於比較簡單的情境,當我們的系統變複雜後,通常會需要繼續細分任務才足以使用,比較主流的就是 Clean Architecture 的方式。
單純從 MVC 的角度看,我們會有這樣的責任分配
- Model - 保存狀態、商業邏輯
- View - 呈現資訊、處理算繪(Render)
- Controller - 接收操作並且協調 Model & View
這種情境通常表示輸入的資料跟狀態通常非常接近或者一致,並且也沒有太複雜的處理邏輯。當使用者填寫的表單跟資料庫有差異時,Controller 就需要做大量的處理,並且 Model 上的驗證機制很可能無法發揮作用。
此時 Form Object 的概念就會被加入進來,把 Controller 和 Model 的職責抽離一部分出來,變成新的類型。
- Model - 保存狀態、商業邏輯
- View - 呈現資訊、處理算繪(Render)
- Controller - 協調 Form 和 Model 並且回傳 View 產生的內容
- Form - 根據輸入轉換成 Model 可以使用格式
然而,當我們希望 Model 的狀態不該被輕易改變,就會需要依靠實作一些方法來協助處理。此時就會從 Model 直接以 #assign_attributes
方法讀取 Form 的內容,轉變為依靠 Service Object 來操作的形式。
- Model - 保存狀態、商業邏輯
- View - 呈現資訊、處理算繪(Render)
- Controller - 協調 Form 和 Service 協作回傳 View 產生的內容
- Form - 根據輸入轉換成 Service 的參數
- Service - 根據參數操作 Model 進行處理
上述這種擴充的過程是比較接近 Clean Architecture 的方式,然而在實作上如果沒有恰當的設計,就會很難擴充成這樣的架構,因此在開發初期仍需要耗費一定成本釐清某些介面(Interface)的存在。
案例
上述的說明仍是相對抽象,我們繼續以物流系統的案例進行說明。在開發初期,我們通常會直接採取 Rails 常見的的方式實作,大致上如下。
1class ShipmentController < ApplicationController
2 def update
3 @shipment.update(shipment_params)
4 # ...
5 end
6
7 private
8
9 def shipment_params
10 params.require(:shipment).permit(
11 items_attributes: %i[shippable_type shippable_id]
12 )
13 end
14end
當我們需要針對不同角色給予權限,或者一性次修改所有運送的品項時,在 Shipment
上實作驗證就會相當複雜,此時拆分為 Form Object 就會是個好方法。
1class ShipmentController < ApplicationController
2 def update
3 @form = Customer::ShipmentForm.new(shipment_params)
4 @form.valid!
5
6 @shipment.update(@form.attributes)
7 # ...
8 end
9
10 # ...
11end
1class Customer::ShipmentForm < BaseForm
2 # ...
3
4 def attributes
5 {
6 items_attributes: formated_items
7 }
8 end
9end
像這樣子,我們可以在 Form 裡面針對 items_attributes
額外做一些處理,雖然也可以用 items
直接對應物件,不過會造成有額外的職責,因此需要避免。
然而,這樣依舊很難針對運送操作進行細部的處理,因此會繼續擴充出 Service Object 來協助我們處理。
1class ShipmentController < ApplicationController
2 def update
3 @form = Customer::ShipmentForm.new(shipment_params)
4 @form.valid!
5
6 @service = Customer::UpdateShipmentService.new
7 @service.call(
8 @shipment,
9 items: @form.items
10 )
11
12 # ...
13 end
14
15 # ...
16end
1class Customer::UpdateShipmentService < BaseService
2 # ...
3
4 def call(shipment, items:)
5 items.each do |item|
6 state = item.delete(:state)
7 case state
8 when :remove then shipment.remove_item(item[:id])
9 when :update then shipment.update_item(item[:id], item)
10 when :add then shipment.add_item(item)
11 end
12 end
13 end
14end
如上述的例子,我們被允許在使用者輸入的資料加入一些額外的屬性,這些屬性不用對應 Shipment
和 ShipmentItem
的資料表欄位,我們可以透過 Form Object 先檢查過這些輸入的內容是否正確,並且轉交給 Service Object 根據提供的內容進行對應的動作。
上述仍是單純的 CRUD 情境,如果是有搭配一些 API 呼叫的情境,使用 Service Object 來進行協調這些處理等等,就更能夠將不同類型的職責區分出來。
如果對這篇文章有興趣,可以透過以下連結繼續閱讀這系列的其他文章。
- 軟體架構的挑戰 - 重新思考 Rails 架構
- 資料驅動設計 - 重新思考 Rails 架構
- 複雜的操作 - 重新思考 Rails 架構
- 時區換算 - 重新思考 Rails 架構
- 報表機制 - 重新思考 Rails 架構
- 通用化功能 - 重新思考 Rails 架構
- ActiveRecord 的限制 - 重新思考 Rails 架構
- 領域驅動設計 - 重新思考 Rails 架構
- 從架構到設計 - 重新思考 Rails 架構
- 重復使用的反思 - 重新思考 Rails 架構
- 釐清脈絡 - 重新思考 Rails 架構
- 劃分邊界 - 重新思考 Rails 架構
- 職責劃分 - 重新思考 Rails 架構
- 架構規劃 - 重新思考 Rails 架構
- 驗收測試 - 重新思考 Rails 架構
- Controller - 重新思考 Rails 架構
- Form - 重新思考 Rails 架構
- Use Case - 重新思考 Rails 架構
- Entity - 重新思考 Rails 架構
- Repository - 重新思考 Rails 架構
- Output - 重新思考 Rails 架構
- Query - 重新思考 Rails 架構
- 可能性 - 重新思考 Rails 架構