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

劃分邊界 - 重新思考 Rails 架構

這篇文章是 重新思考 Rails 架構 系列的一部分。

在領域驅動設計(Domain-Driven Design)中有著上下文邊界(Bounded Context)這樣的概念存在,可以先簡單的理解為協助我們區分不同領域(Domain)的方式。

上下文(Context)我比較喜歡用「脈絡」來描述,當我們已經對脈絡有基本的區分,在對邊界的劃分就會容易很多。

決定邊界

確立邊界的目的在於讓我們知道哪些物件可以互動,如果是在不同的領域就會需要透過特定的方式來互動(如:API)因此會需要幫每個 Model 來選擇管理的範圍。

首先,以訂單(Order)的情境來看,我們希望運送一批貨物(Product)這些貨物是不會憑空出現的,那麼第一個邊界就是 Order + Product 兩個物件所構成的領域(Domain)

接下來則是集裝(Container)的情境,我們會需要知道有哪些商品被組合在同一個運送單元內,此時就需要考慮「是否該關聯商品」這樣的問題。然而,商品已經被劃分為訂單所管理,那麼這裡我們更適合用 PalletItem 這樣的關係來紀錄「某個棧板存在某個商品」這樣的資訊。

 1class Container < ApplicationRecord
 2  has_many :pallets
 3end
 4
 5class Pallet < ApplicationRecord
 6  has_many :items, class_name: "PalletItem"
 7  belongs_to :container
 8end
 9
10class PalletItem < ApplicationRecord
11  belongs_to :pallet
12  belongs_to :product
13end

這樣的設計有可能出現「資料不同步」的問題,另一方面卻也能得到不同的優點(如:減少 Lock 衝突的情況)等等

最後則是運送(Shipment)的情境,有了上述的案例,就很明確的知道不會直接關聯到 Container 上,那麼可能會是一個多型(Polymorphic)關聯,也順帶解決了「運送單位」不同的問題。

1class Shipment < ApplicationRecord
2  has_many :items, class_name: "ShipmentItem"
3end
4
5class ShipmentItem < ApplicationRecord
6  belongs_to :shipment
7  belongs_to :shippable, polymorphic: true
8end

我們後續還有許多問題要解決,也可能需要這樣的設計,至少對於每個領域中該有怎樣的物件以及扮演的角色有了大概的雛形。

聚合

在領域驅動設計中有著聚合根(Aggregate Root)的概念,在實務上如果某個實體(Entity)被這個聚合根所管理,那麼我們就必須統一透過聚合根來操作這個實體,而不應該直接去跟這個實體互動。

聚合根通常也是一種實體

上述的訂單、集裝、運送三個領域,都會發現具備這樣的性質存在,訂單中運送的商品不會憑空出現,一定是透過訂單去增減項目。運送的項目也不會自己長出來,一定是以這筆貨運單為基礎調整。

透過附加這樣的限制,讓邊界的概念更加清晰,也能明確的知道 API 介面是什麼(聚合根所提供的公開方法)從另一個角度來看,也有著「高內聚,低耦合」的效果產生,三個模組實質上可以不完全的互相依賴(移除掉 Rails 的 belongs_to 直接以 product_id 作為參考)且能夠透過某個物件統一完成任務。

另一方面,像這樣清晰劃分後,對於資料庫,一個操作單位(指 Transaction,或者 Unit of Work)也就非常明確,基本上都是圍繞聚合根處理,就比較能避免出現一些處理上互相 Lock 情境。

這種方式的代價是會有資料不同步的問題,如訂單的商品資訊變更後,就要思考如何同步到集裝的商品資訊上