
在專案開發中第一次碰到 DDD (Domain-Driven Design)。之前寫過 .NET MVC, 也學過 Go, 發現很多概念其實跟已經知道的東西有關聯, 特別是 Repository Pattern。這篇記錄一下我怎麼從這些已有的認知去理解 DDD

## MVC 是什麼

MVC 是 Model-View-Controller, 三個角色:

- **Model** — 資料和 business logic
- **View** — 畫面 (HTML, JSON response 等)
- **Controller** — 接收使用者的操作, 呼叫 Model 處理, 把結果交給 View 呈現

```
User action → Controller → Model → Controller → View → User sees result
```

概念上, Controller 是中間人, 它不做邏輯判斷, 只負責協調 Model 和 View

## .NET MVC 實務上的架構

教科書的 MVC 是三層, 但實務上 .NET MVC 的 Model 會再拆。因為把 business logic 跟 DB 存取全塞在 Model 裡, 東西一多就很難維護。所以實際上會拆成:

```
Controller  →  接 HTTP request, 決定呼叫哪個 Service, 回 response
    ↓
Service     →  business logic 寫在這裡 (原本 Model 的 logic 部分)
    ↓
Repository  →  資料庫存取 (原本 Model 的 data access 部分)
    ↓
Entity      →  資料結構 (原本 Model 的 data 部分)
```

View 在 API 的情境下就是 JSON response, 不太需要額外處理

拿「啟用一個品牌」當例子:

```csharp
// Controller — receives HTTP request
[HttpPost("brands/{id}/activate")]
public IActionResult Activate(Guid id) {
    _brandService.Activate(id);
    return Ok();
}

// Service — all business logic lives here
public void Activate(Guid id) {
    var brand = _repo.GetById(id);
    if (brand.Status == BrandStatus.Archived)
        throw new InvalidOperationException("Cannot activate archived brand");
    brand.Status = BrandStatus.Active;
    _repo.Save(brand);
}

// Entity — just a data container, no logic
public class Brand {
    public Guid Id { get; set; }
    public string Name { get; set; }
    public BrandStatus Status { get; set; }
}
```

`Brand` 這個 entity 只有 property, 沒有任何方法, 就是一個裝資料的容器。所有的判斷邏輯 (「archived 不能 activate」) 都在 Service 裡。這種 entity 叫做 **anemic entity** (貧血 entity) — 有資料, 沒有行為

## Repository Pattern

Repository Pattern 的概念:

> 用一個 interface 把「資料怎麼存取」抽象化, 讓 Service 不需要知道底層是 SQL Server、PostgreSQL 還是記憶體

```csharp
// Interface — defines the contract
public interface IBrandRepository {
    Brand GetById(Guid id);
    void Save(Brand brand);
}

// Implementation — actual database access
public class SqlBrandRepository : IBrandRepository {
    private readonly DbContext _db;

    public Brand GetById(Guid id) {
        return _db.Brands.Find(id);
    }

    public void Save(Brand brand) {
        _db.Brands.Update(brand);
        _db.SaveChanges();
    }
}
```

透過 DI (Dependency Injection) 把 interface 跟 implementation 接起來:

```csharp
// Startup.cs
services.AddScoped<IBrandRepository, SqlBrandRepository>();
```

Service 只依賴 `IBrandRepository` interface, 不依賴具體的 SQL 實作。想換 DB 或寫測試時, 換 implementation 就好

知道這些之後, DDD 就不難了, 因為 Repository Pattern 本來就是 DDD 裡面的東西

## DDD 到底多了什麼

DDD 全名 Domain-Driven Design (領域驅動設計)。一開始聽到覺得很抽象, 但搞懂之後發現核心就一件事:

> **把 business logic 從 Service 搬回 Entity 自己身上**

同樣「啟用品牌」, DDD 的寫法:

```python
# Entity — has behavior, not just data
class Brand:
    name: str
    status: BrandStatus = BrandStatus.PENDING

    def activate(self):
        if self.status == BrandStatus.ARCHIVED:
            raise ValueError("Cannot activate archived brand")
        self.status = BrandStatus.ACTIVE
```

```python
# Service — becomes thin, just orchestration
class BrandService:
    def activate(self, brand_id: UUID) -> Brand:
        brand = self._repo.find_by_id(brand_id)  # load
        brand.activate()                           # call domain method
        self._repo.save(brand)                     # save
        return brand
```

比較一下:

| | .NET MVC | DDD |
|--|----------|-----|
| Entity | 只有 property (anemic) | **有 property + 方法 (rich)** |
| Service | 判斷邏輯全在這 | 只負責 load → call → save |
| Repository | interface + implementation | 一樣的概念 |

**Repository 的部分完全沒變**。差別只在 business rule 放在哪

### 為什麼要搬?

有 10 個 Service 都會操作 `Brand` 的情況下, .NET MVC 的寫法會讓「archived 不能 activate」這條規則散落在各個 Service 裡, 很容易有人漏掉

DDD 的話, 這條規則寫在 `Brand.activate()` 裡, 不管誰呼叫都會檢查。Entity 自己保護自己的狀態

## DDD 的核心概念

### Entity (實體)

有唯一 ID, 有行為, 會變化

```python
class User:
    id: UUID
    display_name: str
    is_private: bool

    def make_private(self):
        self.is_private = True
```

跟 .NET Entity 的差別就是多了方法。兩個 Entity 的比較方式是看 ID, 不是看 property 的值

### Value Object (值物件)

沒有 ID, 比較的是「值」。不可變

```python
@dataclass(frozen=True)
class CommissionRate:
    basis_points: int  # e.g. 1500 = 15%

    def __post_init__(self):
        if not (0 <= self.basis_points <= 10_000):
            raise ValueError("Must be 0-10000 bps")
```

比喻: 一張一百元鈔票跟另一張一百元鈔票, 不在乎是「哪一張」, 只在乎金額。這就是 Value Object — 比較值, 不是身分

.NET 裡最接近的是 C# 9 的 `record`, 預設 value-based equality

### Aggregate (聚合)

這個概念在 .NET MVC 裡沒有, 我花了一段時間才搞懂

Aggregate 就是一組一定要一起存取的東西, 有一個 root entity 當入口。像是 `Post` (貼文) 跟它的 `Comment` (留言):

```
Post (aggregate root)
├── Comment 1
├── Comment 2
└── Comment 3
```

規則:
- 外面只能碰 root (`Post`), 不能直接操作 `Comment`
- 存取時整個 aggregate 一起 load / save
- 不同 aggregate 之間只用 ID 互相參考, 不持有對方的 object

第三點跟 .NET EF Core 的 navigation property 剛好相反。EF Core 可以寫 `post.Author.Name` 直接拿到 User 物件, DDD 不這樣做 — 只存 `author_id`, 要拿 User 就自己去查

理由是: 如果 Post 直接持有 User 物件, 改 User 的時候就要擔心 Post 那邊會不會壞。用 ID 切開, 每個 aggregate 管好自己

### Bounded Context (限界上下文)

怎麼決定哪些東西放在同一個模組?

DDD 的做法是看「語言」。同一個詞在不同場景代表不同意思, 就是不同的 context

像是 「User」, 對社交功能來說是「有 follow、有 profile 的人」; 對錢包來說是「有餘額、有交易紀錄的帳戶」。雖然都叫 User, 關心的面向完全不同, 所以 Users module 和 Ledger module 是兩個 Bounded Context

專案裡的切法:

| Context | 關心什麼 |
|---------|---------|
| Users | 身分、profile、follow、策展 |
| Brand | 品牌資料、Shopify 整合 |
| Ledger | 錢包、交易、佣金 |
| Affiliate | 聯盟行銷、佣金比例設定 |
| Community | 貼文、留言 |

## Go 的做法

學 Go 的時候好奇過: Go 社群有在用 DDD 嗎?

結論是有, 但不常見, 做法也比較鬆散。Go 社群重視 simplicity, 不太喜歡多層抽象, 所以完整的 DDD 分層在 Go 專案裡很少看到。標準做法還是 handler → service → repository, 跟 .NET MVC 類似

不過有幾個 DDD 概念在 Go 裡很自然:

Go 的 interface 是 implicit (不用寫 `implements`), 定義 Repository contract 很直覺:

```go
type BrandRepository interface {
    FindByID(ctx context.Context, id uuid.UUID) (*Brand, error)
    Save(ctx context.Context, brand *Brand) error
}
```

Entity with behavior 用 method on struct:

```go
func (b *Brand) Activate() error {
    if b.Status == StatusArchived {
        return errors.New("cannot activate archived brand")
    }
    b.Status = StatusActive
    return nil
}
```

Python 的 Protocol (structural typing) 跟 Go 的 implicit interface 概念一樣 — 不用宣告「我實作了什麼 interface」, method signature 對上就算實作。跟 C# 要寫 `class Foo : IFoo` 明確宣告很不一樣

## 為什麼不直接用 .NET MVC 的模式

Repository Pattern 已經會了, 為什麼還要 DDD?

因為專案越來越大的時候, 碰到了幾個 .NET MVC 模式處理不好的問題:

1. **Service 越來越胖** — 所有 logic 都在 Service, 一個 BrandService 幾百行, 改一個地方要讀完整個檔案
2. **規則散落各處** — 同一個 business rule 在多個 Service 裡各寫了一次, 改了一個忘了改另外的
3. **模組之間糾纏** — UserService import BrandService, BrandService import WalletService, 蜘蛛網
4. **不知道邊界在哪** — 想拆模組但不知道從哪裡切

DDD 的解法:
- 問題 1 → entity 自己帶 logic, service 變薄
- 問題 2 → rule 只寫在 entity 上, 單一來源
- 問題 3 → 跨模組只能透過 presentation 層接線, 不能直接 import
- 問題 4 → Bounded Context 提供切模組的判斷標準

## 整理

| .NET MVC 已知概念 | DDD 對應 | 差在哪 |
|-------------------|----------|--------|
| Entity (property only) | DDD Entity | DDD 的 entity 有行為 |
| Repository Pattern | DDD Repository | 幾乎一樣, DDD 更嚴格 (只 return aggregate root) |
| Service | Application Service | DDD 的 service 更薄, 只做 load → call → save |
| Controller | Presentation (route) | 概念一樣 |
| DI Container (Startup.cs) | Composition Root (providers.py) | 概念一樣, 位置不同 |
| 沒有直接對應 | Value Object | 不可變, 比較值 |
| 沒有直接對應 | Aggregate | 一組一定要一起存取的東西 |
| 沒有直接對應 | Bounded Context | 用「語言」來切模組邊界 |

下一篇記錄實際在專案裡踩到的坑: aggregate boundary 畫錯、double-entry ledger 怎麼設計、event system 怎麼讓模組之間溝通

## References

- [Domain-Driven Design Reference — Eric Evans](https://www.domainlanguage.com/ddd/reference/)
- [Martin Fowler — AnemicDomainModel](https://martinfowler.com/bliki/AnemicDomainModel.html)
- [Martin Fowler — BoundedContext](https://martinfowler.com/bliki/BoundedContext.html)
- [Martin Fowler — DDD Aggregate](https://martinfowler.com/bliki/DDD_Aggregate.html)
- [Martin Fowler — ValueObject](https://martinfowler.com/bliki/ValueObject.html)
- [Martin Fowler — Repository](https://martinfowler.com/eaaCatalog/repository.html)
- [Microsoft — ASP.NET MVC Pattern](https://dotnet.microsoft.com/en-us/apps/aspnet/mvc)
- [Microsoft — Repository Pattern](https://learn.microsoft.com/en-us/dotnet/architecture/microservices/microservice-ddd-cqrs-patterns/infrastructure-persistence-layer-design)
- [Python dataclasses — frozen](https://docs.python.org/3/library/dataclasses.html#frozen-instances)
- [Python typing — Protocol](https://docs.python.org/3/library/typing.html#typing.Protocol)
- [Go — Effective Go (Interfaces)](https://go.dev/doc/effective_go#interfaces)
