---
title: "聚合與邊界 - Rails 開發實踐"
date: 2023-10-20T00:00:00+08:00
publishDate: 2023-10-20T00:00:00+08:00
lastmod: 2023-09-03T17:33:12+08:00
tags: ["經驗","心得","Rails","Rails 開發實踐"]
series: "rails-in-practice"
toc: true
permalink: "https://blog.aotoki.me/posts/2023/10/20/rails-in-practice-aggregate-and-boundary/"
language: "zh-tw"
---


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

<!--more-->

## 聚合{#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 會這樣實現。

```ruby
class Subscription < ApplicationRecord
  # ...
  def total_amount
    items.sum(&:amount)
  end
end
```

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

## 邊界{#boundary}

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

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

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

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

