蒼時弦也
蒼時弦也
資深軟體工程師
發表於

領域驅動設計 - 重新思考 Rails 架構

這篇文章是 重新思考 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 架構做準備。