iT邦幫忙

2024 iThome 鐵人賽

DAY 6
0

Tactical Design

接續上一章節的成果。
Strategic Design 11

我們現在要來設計 Aggregates 的細節了。

在實作之前,我們先來介紹 DDD 三個重要的物件規則:Entity、Aggregate 和 Value Object:

1. Entity

  • 具有一個不可變(immutable)且唯一的 ID。
  • 只要兩個 Entity 的 ID 相同,無論其他屬性和狀態如何,它們就是相同的物件。
  • 除了 ID 之外,Entity 的其他屬性可以被改變,並且它有自己的生命周期,如 CRUD(新增、讀取、更新、刪除)操作。

2. Value Object

  • Value Object 一旦創建後就不會變更。如果需要改變,應該直接創建新的實例。
  • Value Object 與 Entity 不同,它沒有唯一的 ID。它的價值完全取決於其屬性值,而非其身份。
  • 如果兩個 Value Object 的所有屬性值都相等,那麼它們就是相同的 Value Object。

3. Aggregate

  • Aggregate 定義了內部的邊界上下文(Boundary Context),所有操作都必須確保 Aggregate 內部狀態的正確性。
  • Aggregate 是由多個 Entity 和/或 Value Object 組成的集合,並被視為一個整體。
  • 每個 Aggregate 都有一個 Aggregate Root,Aggregate Root 本身就是一個 Entity,而 Aggregate Root 的 ID 用來識別整個 Aggregate。
  • 所有對 Aggregate 的外部操作都應該通過 Aggregate Root 來執行。
  • Aggregate Root 負責管理 Aggregate 的整個生命周期,並確保在事務範圍內的操作是原子的,即所有操作要麼全部成功,要麼全部回滾,以保持 Aggregate 的一致性。
  • 不同 Aggregate 之間不應該直接引用對方的實體,而是應該通過引用對方的聚合根 ID 來進行交互,這樣可以避免強耦合和循環依賴。
  • 跨 Aggregate 的操作應避免在同一事務中執行,不同的 Aggregate 之間需要保持操作隔離,以避免併發性問題。

設計 Aggregates 內部的細節

接著,我們要來看看這些 Aggregates 內部要如何設計,先把我們前一章的所有 Aggregates 列出來,寫下他們的 Aggregate root 作為 Entity,然後把 Aggregate 內所有 Entity 的關係寫下來。
Tactical Design 01
接著按照:

不同 Aggregate 之間不應該直接引用對方的實體,而是應該通過引用對方的聚合根 ID 來進行交互,這樣可以避免強耦合和循環依賴。

我們把 Entity 的關係改為依賴 ID (Value Object)。
Tactical Design 02

Todo Item Aggregate 中新增 Value Object: Status

我想要在 Todo Item Aggregate 裡面多加一個 Value Object: Status。這個 Todo Item 在不同狀態下會有不同的顏色,範例如下:

{
  "todoItemStatus": {
    "status": "todo",
    "color": "#FFFFFF"
  }
}

結果會變成這樣。
Tactical Design 03

設計 Entity 和 Value Object 的資料格式

User

{
  "id": { "value": "00000000-0000-0000-0000-000000000000" },
  "firstName": "Tiffany",
  "lastName": "Doe",
  "email": "user@gmail.com",
  "password": "p@ssw0rd!", // Need hash
  "createdDateTime": "2024-01-01T00:00:00.0000000Z",
  "updatedDateTime": "2024-01-01T00:00:00.0000000Z"
}

Todo List

{
  "id": { "value": "00000000-0000-0000-0000-000000000000" },
  "name": "Default",
  "description": "Default",
  "status": "active", // active, removed
  "userId": { "value": "00000000-0000-0000-0000-000000000000" },
  "itemIds": [
    { "value": "00000000-0000-0000-0000-000000000000" }
  ],
  "createdDateTime": "2024-01-01T00:00:00.0000000Z",
  "updatedDateTime": "2024-01-01T00:00:00.0000000Z"
}

Todo Item

{
  "id": { "value": "00000000-0000-0000-0000-000000000000" },
  "content": "Do homework!",
  "todoItemStatus": {
    "status": "todo", // todo, finished, removed
    "color": "#FFFFFF"
  },
  "listId": { "value": "00000000-0000-0000-0000-000000000000" },
  "createdDateTime": "2024-01-01T00:00:00.0000000Z",
  "updatedDateTime": "2024-01-01T00:00:00.0000000Z"
}

Aggregates 設計與 Database 無關

到目前為止,所有的設計都圍繞著開發需求 (Domain)。在這之前的所有設計都不應該被任何 Database 相關的議題所干涉,這才是 Domain-Driven 的正確方式。

注意事項

  • Aggregate ID 是 Aggregate 與外部交互的唯一 ID,需要全系統唯一。
  • Entity ID 是在 Aggregate 內交互,需要在該 Aggregate 內唯一(這次實作上沒有 Aggregate Root 以外的 Entity)。
  • Value Object 沒有 ID。
  • 一個 Aggregate 的改變不會影響到其他 Aggregates。

設計 Database 結構

現在,所有的邏輯與需求都被設計好了,我們可以來討論 Database 該如何配合這個結果。

我們先把資料攤平到 Table 上,加上一些 Constraints,就會發現這個模型相當直觀。

Tactical Design 04

唯一要注意的是 TodoItemStatus 這個 Value Object,因為它沒有自己的生命週期,也就是說它的生命週期跟著 Entity/Aggregate,所以我們選擇將它攤平到 Table 內。接著是 SQL 語法:

CREATE TABLE [dbo].[tb_user] (
    [id] UNIQUEIDENTIFIER NOT NULL PRIMARY KEY,
    [first_name] NVARCHAR(100) NOT NULL,
    [last_name] NVARCHAR(100) NOT NULL,
    [email] NVARCHAR(255) NOT NULL UNIQUE,
    [password_hash] NVARCHAR(255) NOT NULL, 
    [created_date_time] DATETIME2 NOT NULL,
    [updated_date_time] DATETIME2 NOT NULL,
    CONSTRAINT UQ_User_Email UNIQUE (email)
);

CREATE TABLE [dbo].[tb_todo_list] (
    [id] UNIQUEIDENTIFIER NOT NULL PRIMARY KEY,
    [name] NVARCHAR(100) NOT NULL,
    [description] NVARCHAR(255) NOT NULL,
    [status] NVARCHAR(50) NOT NULL CHECK (status IN ('active', 'removed')), 
    [user_id] UNIQUEIDENTIFIER NOT NULL,
    [created_date_time] DATETIME2 NOT NULL,
    [updated_date_time] DATETIME2 NOT NULL,
    FOREIGN KEY (user_id) REFERENCES [dbo].[tb_user](id)
);

CREATE TABLE [dbo].[tb_todo_item] (
    [id] UNIQUEIDENTIFIER NOT NULL PRIMARY KEY,
    [content] NVARCHAR(255) NOT NULL,
    [status] NVARCHAR(50) NOT NULL CHECK (status IN ('todo', 'finished', 'removed')),
    [color] NVARCHAR(7) NOT NULL, -- 例如 HEX 顏色代碼
    [list_id] UNIQUEIDENTIFIER NOT NULL,
    [created_date_time] DATETIME2 NOT NULL,
    [updated_date_time] DATETIME2 NOT NULL,
    FOREIGN KEY (list_id) REFERENCES [dbo].[tb_todo_list](id)
);

到這裡,資料結構的設計大致上完成了,剩下一些資料流的設計我們等實戰部分再來解釋。

另外,這個 Database 的設計只是初步的概念,等到實作會有些微的不同。

思考問題:Todo Item 變成 Entity?

想拋出一個問題給大家思考:在做 Aggregates 設計時,Aggregate 是允許被別的 Aggregates 聚合的,那麼我們是否可以將 Todo Item 變成 Todo List Aggregate 中的一個 Entity?如果可以,最終結果會是怎樣?這樣設計又有什麼好處與壞處?


上一篇
Day 05 - DDD Strategic Design:To-Do List 系統需求分析與架構規劃
下一篇
Day 07 - 微服務基本功:gRPC 與 GraphQL
系列文
DDD? Clean Architecture? Microservices? 帶你用.NET實作打造一個現代化微服務!30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言