---
title: "我對 Domain-Driven Design 的理解（2022 年）"
date: 2022-10-21T00:00:00+08:00
publishDate: 2022-10-21T00:00:00Z
lastmod: 2022-10-18T20:47:39+08:00
tags: ["心得","Domain-Driven Design"]
toc: true
permalink: "https://blog.aotoki.me/posts/2022/10/21/my-understand-of-domain-driven-design/"
language: "zh-tw"
---


因為把 Domain-Driven Design: The First 15 years 看完後腦中有了非常多想法，為了不要太快把這些東西忘記，只能盡快寫成文章記錄下來。

簡單來說，我認為 Domain-Driven Design 是一種「觀念」而不是理論或者實踐，他更接近於將「模型（Model）」的概念帶入到程式設計中，也因此在書中提到 Domain-Driven Design 是沒有嚴格規定，只有接近跟遠離核心的差異。

<!--more-->

## 建構模型{#modeling}

實作一個軟體的方式很多，而 Domain-Driven Design 的一切討論是基於模型（Model）的概念，也就是我們該如何把「現實世界存在的資訊轉換為程式」並且讓程式來幫我們處理這些問題，這也讓像是 Value Object（值物件）跟 Entity（實體）這樣的概念在實作中被界定出來，因為我們需要找出一些跟現實世界的關聯性。

然而，這件事情其實是受到程式語言、領域上的影響。以最近很熱門的人工智慧領域來看，機器學習（Machine Learning）建構「模型」的方式是利用大量訓練資料來建構，而 Domain-Driven Design 則是透過觀察、分析來決定。

這都會影響到我們最後怎麼去實作、定義，以書中其中一個章節的 F# 範例來看，在 F# 中可以做到這樣的事情。

```fsharp
type EmailOnly = Email of string
type AddressOnly = Address of string
type BothMethods = {
  EmailOnly: EmailOnly,
  AddressOnly: AddressOnly
}

type ContactInfo =
  | EmailOnly
  | AddressOnly
  | BothMethods
```

像這樣很直覺的就可以建造出只有 Email 或 Address 或者兩者同時存在的情況，並且「其中一個符合」的定義出來，相比其他語言更加直觀。

## 觀察世界{#observe-the-world}

既然要對真實世界進行「建模（Modeling）」那麼我們就需要對世界進行觀察，而 Event Storming（事件風暴）是其中一種手段，如果我們有更好的方式，並不一定要採用這個方法，然而大多數時候這個方法更加容易。

Event Storming 的觀察方式是將現實世界以時間順序為基礎，以「事件」作為節點來描述一件事情，並且最終會停在某個事件作為結束，有點類似大眾所知的量子力學「在觀察之前不會知道結果，最終會得到一個結果」的感覺，我們透過描述被觀測到的結果來檢查在這個領域中會出現的情況。

接下來進一步的將觀察的資訊詳細描述，加入參與者（Actor）、觸發事件的行為（Command）、影響結果的因素（Data or Read Model）、造成事件的機制（System）、事件產生的影響（Policy）來完善這個模型，讓我們得以從中找出 Value Object、Entity 這類在實作上所需的線索，以及對整個領域中造成影響的範圍建構概念，進而得到 Bounded Context（界限上下文）

> 這個過程也是建構 Ubiquitous Language（共通與言）的好時機，因為所有人都在用自己的方式描述這個模型，大量溝通的過程中就會逐步地將在特定「語言」完善，有點類似第一個翻譯語言的人，雙方用各自的語言描述相同的事物的感覺

## 與模型互動{#interact-with-model}

當我們能夠將現實世界的事物以一個模型來描述時，我們還需要搭建一個可以跟這些模型互動的環境，也因此 Layered Architecture（層級式架構）這類方案就被使用在這之中，用來建構一個可以跟模型互動的環境。

以 Layered Architecture 為例子，Presentation Layer（表現層）劃分出了使用者對這個模型的觀察，可能是顏色、狀態等等。而 Application Layer（應用層）則描述了如何跟模型互動的方式，至於 Infrastructure Layer（基礎層）就負責提供實現這個模型的基底。

## 模型玩具的例子{#example-of-toy}

我們用「迴力車」來作為例子，以 Event Storming 的角度來看，我們會觀察到「車子往前」這樣的事件，並解加入操作者（小孩）、往後拉（最多只能 3 公分）、因為彈簧驅動車子向前等等資訊。

如果將其轉換成 Domain Model 我們可以先從「資料（Data）」的方式轉換成有意義的「資訊（Infromation）」那麼會先得到一個「能量」的資訊來表示向後拉的數值，同時「最多只能 3 公分」會被定義在這之中，因為這個「能量」僅限於這個領域。

```ruby
class Power
  include Comparable

  # 描述 Domain Knowledge
  MAX_VALUE = 3
  MIN_VALUE = 0

  attr_reader :value

  def initialize(value = 0)
    @value = value
  end

  def ==(other)
    @value == other.value
  end

  def +(power)
    charged_value = [value + power.value, MAX_VALUE].min
    constrained_value = [MIN_VALUE, charged_value].max

    Power.new(constrained_value)
  end
end
```

接下來 Value Object 就可以繼續組成 Entity 來表示一個「模型」的基礎單位，並且具備了改變自身狀態的行為能力「蓄力（Charge）」和「前進（Run）」

```ruby
class Car
  # 對 Domain Modle 們來說只認識有意義的 Value Object 而不是 Data
  NO_POWER = Power.new(0)
  STEP_POWER_USAGE = Power.new(1)

  attr_reader :id, :power

  def initialize(id:, power: NO_POWER)
    @id = id
    @power = power
  end

  def charge(power)
    @power += power
  end

  def run
    @power -= STEP_POWER_USAGE
  end
end
```

根據領域的複雜程度，我們的 Entity 可能是一個更大模型的一個零件，還會有 Aggregate（聚合）以及多個模型要互動時需要的 Service（服務）

然而，我們只有「領域模型（Domain Model）」是不足以讓使用者使用，因此在基於架構方面的理論，建構出一個足以使用的環境。

```ruby
class RunCarService
  def run(car)
    return until car.power == Car::NO_POWER

    car.run
    puts "The car is running..."
  end
end

class CarController
  def play(input)
    # ToyBox 是 Repository 用於表示「玩具」的集合
    car = ToyBox.find_a_card("Aotoki's blue car")
    # Ruby 的 Hash 是天然的 DTO 因此沒有另外實作 ChargeInput
    power = Power.new(input[:value])

    # Application Layer 以描述行爲的「步驟」為主
    car.charge(power)
    run_car_service.run(car)
  end
end
```

在 `CardController` 我們可以看到能夠使用一種叫做 `#play` 的方法來讓這台車動起來，並且只需要提供「能量的數值」

> 為了控制篇幅這邊稍微簡化到只保留 Application Layer 的部分

然而，我們仍在上面的例子看到一些不太合理的地方，像是該如何解釋 Input/Output 的來源以及去向、該如何構成一個「行為」以及操作者的角色是誰等等，也因此像是 Clean  Architecture 或者 DCI（Data Context Interaction）等等不同理論也被提出或者加入到 Domain-Driven Design 的討論中，用以改善這些不完善的部分。

> Domain-Driven Design: The First 15 Years 中有提到 Eric Evans 認為當初寫下的藍皮書（Domain-Driven Design 原作）是不夠完善的，也因此在 Domain-Driven Design: The First 15 Years 這本書中有很多「解釋」跟「批判」和「補充」類型的章節，持續的去完善整個體系。

## 已知與未知{#known-and-unknown}

從 Event Storming 或者建模的過程中，不難看出來整個實踐是基於「已知」的前提去進行的，也因此「領域專家（Domain Expert）」就變得非常重要，因為我們需要在過程中盡可能的提取知識，來確保整個模型的建構是足夠完善的。

這也是爭議的一部分，也就是會有人認為「花太多時間」或者「過度設計」的狀況存在，雖然在 Domain-Driven Design: The First 15 Years 中有參考了敏捷開發（或極限編程）的概念，但也是有些人選擇「融入」有些人選擇「否定」這樣的方式。

然而，模型在 Domain-Driven Design 中也是會逐漸改變的，至少現實世界中的「模型」也會隨著技術發展、使用者的變化（長大）有些許的轉變，因此我在這件事情上的觀點更傾向於「情境」本身決定實踐方式。

舉例來說，我們已經是一個成熟的「企業」那麼利用 Event Storming 來了解原有的系統並且界定出界限上下文、子領域，是非常合理的情境。另一方面，在 Domain-Driven Design 被提出的時期，也差不多是敏捷開發開始逐漸成熟的時期，某方面來說敏捷開發是對應了對「未知」探索的方法，也因此原本 Domain-Driven Design 用來處理「已知」的方法可能就不太適合。

以我今年最喜歡的 Acceptance Test Driven Development（驗收測試驅動開發，A-TDD）作為例子，以由上而下的方式逐漸探索一個系統，實際上也足以打造一個可用的軟體，跟 Event Storming 的觀察方式剛好完全相法，一個是「黑箱」的方式觀察，另一個則是「白箱」的方式觀察。

實際上，在「建模」這件事情上可能是沒有差異太大，需要專注的是 Domain（領域）的邊界跟劃分以及減少造成昂貴修改成本的情況，從敏捷開發的角度勢必會有一段時間是單體的（Monolithic），直到我們將邊界探索完畢。

## 開放心態{#open-mind}

整體觀察下來，其實 Domain-Driven Design 對我來說更接近是一種觀念，或者說「Model-based（以模型為基礎）」的程式設計的實踐方式，同時整個社群的目標也不外乎是思考「如何建立適合的模型」並且以此為目標努力。

基於這樣的理由，假設有一天 Top-down（自上而下）的探索方式能夠提供「對未知探索」的建模足夠完善的解決方案，也許會被寫在新出版的書中。又或者像是 DCI 這個針對 MVC 的補充，在 15 年後被加入到書中用來呈現在怎樣的情況下，可以去補足原本 Domain-Driven Desing 的不足。

之前在發文時也有人提到「基本教義派」這樣的存在，不過也很難去界定說怎樣是「基本教義派」的定義，不過 Domain-Driven Design 的邊界隨時都在變化的狀況下，我想「基本教義派」其實就是那些在心中建立一個「固定範圍」的人們，所以我們才會覺得他們的建議「過時」跟「不合理」因為沒有隨著整個領域一起「進步」

最後，我會選擇把 Domain-Driven Design 的理論忘掉，或者說「精煉」出來。一方面是我專注的領域更多是 Startup（新創）類型的專案，因此「已知」的探索幫助並不大，同時我主要使用的是 Rails 框架，不論是 Ruby 語言或者 Rails 框架對於現有的 Domain-Driven Design 架構都是不夠契合的，即使 Rails 框架其實隱含了非常多 Domain-Driven Design 的概念，這也就表示需要去「探索」適合用在 Rails 框架的方法，而不是在已知的方法中想辦法「套用」

當然，我很喜歡 Domain-Driven Design 帶給我很多觀念上的衝擊，以及解答非常多實作上遇到的疑惑，因此我還是會持續關注、探索，直到有一天我確定我專注的開發技巧可以被稱為 Domain-Driven Design 或者無法。

