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

聚合與邊界 - Rails 開發實踐

這篇文章是 Rails 開發實踐 系列的一部分。

倉庫跟實體是相當基本的概念,然而還不足以涵蓋更多的情境。我們還需要討論 Aggregate(聚合)的情況,以我們這次的例子來說,就是一種聚合的表現。有了 Aggregate 的概念後,就可以逐步看出一個系統的邊界。

聚合

Aggregate 和 Entity(實體)基本上是非常類似的,他們通常都會有一個唯一的編號(如:ID)來識別成獨立的個體,然而差異在於 Aggregate 會有從屬的 Entity 而且這些 Entity 的改變會由 Aggregate 來管理。

以訂閱機制的情境作為例子,Subscription 聚合了 SubscriptionItem 也因此當我們要製作新的 SubscriptionItem 不會使用 SubscriptionItem.new(subscription_id: ...) 的方式,而是 subscription.items.build(...) 來處理。

這也是我們會以 #extend_with(...) 方法來建立新的訂閱的原因,基本上我們會希望這些被聚合的 Entity 是由他的 Aggregate Root(聚合根)來進行管理。

Aggregate 的特性也跟我們在資料庫經常使用的 Aggregate Function(聚合函式)類似,像是 SELECT SUM(amount) FROM ... 在 Aggregate 會這樣實現。

1class Subscription < ApplicationRecord
2  # ...
3  def total_amount
4    items.sum(&:amount)
5  end
6end

因為是 ActiveRecord 的關係,我們可以用 items.sum(:amount) 來直接對資料庫查詢,而不是載入所有 SubscriptionItem 後由 Ruby 來加總。

邊界

你可能會發現,如果是 Aggregate 的話在 Rails 中就是使用 has_many 來關聯其他 Model 的物件,然而依照 Aggregate 的定義,如果 User 關聯了相關的 Model 後,不就變成所有操作都需要以 User 來控制了?

這就要提到上下文邊界(Bounded Context)的概念,在一個系統中可能會有訂閱、會員、商品等等不同的模組,這些模組完全可以自己構成一個小的系統,並且在大多數時候互不關聯。

正因如此,我們在 User 中不會用 has_one 或者 has_many 來關聯 Subscription 而是利用 Repository(倉庫)的方式,用 Subscription.by_owner(user) 來篩選出我們想要關聯的資料。

如此一來,原本 UserSubscription 兩個物件需要互相知道對方資訊的情況,就變為約定「具有 #id 屬性的物件」這樣一層相對薄弱的關係,兩者就劃分出了邊界。透過這樣的模組劃分,我們就可以把相關的邏輯有效的聚集在一處,降低維護上的困難。