領域驅動設計 - 重新思考 Rails 架構
在 Rails 中除了以模型(Model)為出發點思考如何設計,我們也可以從領域(Domain)的角度進行思考,雖然 Rails 的架構對於領域驅動設計(Domain-Driven Design)的應用一直都是社群想挑戰的主題,仍沒有主流的方式,然而從不同的角度思考仍是有助於設計更好的系統。
職責
在學習物件導向時,很常會聽到單一職責(Single Responsibility)這樣的思考方式,我認為領域也是類似的思考方式,都是用於釐清某個物件或者功能應該涵蓋的範圍。
延續「分庫出貨」的實作,我們在思考系統的設計時,應該要先討論訂單(Order)和運送(Shipping)是否為同一件事情,這並沒有一個絕對的答案,基本上會取決於當下的情境和脈絡。
這裡我們先假設訂單跟運送是兩個不同領域的任務,因此我們要讓兩者可以專注在各自的責任上。那麼,原本 Rails 經常會像這樣設計關聯,可能不太適合。
1class Order < ApplicaitonRecord
2 has_many :shippings
3
4 # ...
5end
因為在我們的概念中訂單和運送是兩個獨立的模組,因此在操作訂單時不應該去觸碰到運送相關的任務,那麼在 Model 的使用上,不要去定義關聯會是更好的。
流程
如果要實現當所有品項送達時,更新訂單狀態的設計,就不會在 Model 上面進行處理,因為這是橫跨兩個不同領域的任務。
這個情境就很適合在 Controller 上進行,因此在實作 Controller 的時候會去描述兩個模型之間的互動是怎樣的。
1class ShippingState < ApplicationController
2 # ...
3 def update
4 # Step 1
5 @shipping.shipped!
6
7 # Step 2
8 completed = Shipping.for(@shipping.order_id).all?(&:shipped?)
9
10 # Step 3
11 if completed
12 order = Order.find(@shipping.order_id)
13 order.shipped!(at: @shipping.shipped_at)
14 end
15 end
16end
在這裡我們的處理一共有三個步驟,第一個步驟是將運送標記成「送達」狀態,接下來我們會確認同一筆訂單的運送狀態是否都是送達的,假設滿足條件就會進到最後的階段將訂單也更新成送達狀態。
這樣的設計還有另一個好處,就能有一定程度的冪等性(Idempotency)及最終一致性(Eventual consistency)
從冪等性的角度來看,即使中間處理失敗,只要狀態沒問題,再次執行就能夠繼續下去。最終一致性的角度來看,不論哪一筆運送紀錄先完成,最後都能夠讓訂單狀態進入到「已送達」的狀態,即使發生問題,只要再次執行任一筆運送紀錄的處理,也還是會變成「已送達」
這樣也更能解釋清楚在模型驅動設計時,我們認為訂單會需要狀態跟送達時間,卻又無法解釋在 has_many :shippings
的前提下,我們為什麼會有這樣欄位的狀況,如果是從職責、流程的角度來分析,因為是不同領域的問題看似「重複」但是「意義不同」是經常發生的。
上述的 Controller 設計會讓 Controller 變得複雜,因此在 Rails 社群經常會使用 Service Object 的方式處理,然而我認為他更適合用 Command Pattern 來處理,也能作為銜接 CQRS 架構做準備。
如果對這篇文章有興趣,可以透過以下連結繼續閱讀這系列的其他文章。
- 軟體架構的挑戰 - 重新思考 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 架構