📌 30 秒摘要(Layer 3)

企業應用(有大量業務邏輯 + 資料庫存取)的模式目錄。Fowler 把「業務邏輯該怎麼組織」與「物件如何對應到資料庫」這兩個反覆出現的難題,整理成一組有名字的解法。你今天用的 ORM(Active Record / Data Mapper)、分層架構、Unit of Work、Repository,源頭都在這裡。理解這些模式,就懂了框架(Rails、Hibernate、Prisma…)底層在做什麼。

🗺 心智圖(Canvas)

企業應用架構模式

🏛 企業應用架構模式

PoEAA

領域邏輯三選項 Transaction Script(程序式)/Domain Model(物件化)/Table Module

資料源二選一 Active Record(簡單)vs Data Mapper(乾淨但複雜)

分層 展現層/領域層/資料源層,各司其職

🎯 什麼情境該想到我

當你的業務邏輯「散落各處、service 層越來越肥、不知道邏輯該放哪」時。

⚙️ 怎麼用(依業務複雜度選)

  1. Transaction Script(交易腳本):一個動作=一段程序式流程。簡單業務用它最直接,別過度設計。
  2. Domain Model(領域模型):把業務邏輯放進有行為的領域物件(充血模型),而非只有 getter/setter 的貧血物件 + 肥 service。複雜、多變的業務用它才管得住(呼應 領域驅動設計)。
  3. Table Module:以「一張表對應一個類別」組織邏輯,介於兩者之間。

判斷:業務越複雜、規則越多變 → 越該用 Domain Model;越簡單 → 越該用 Transaction Script。 別為簡單 CRUD 硬套領域模型。

★ 三種模式的具體長相

① Transaction Script(交易腳本)

// 一個使用案例 = 一段從頭到尾的程序
func (s *Service) PlaceOrder(ctx context.Context, req Request) error {
    items, err := s.repo.LoadItems(ctx, req.ItemIDs)
    if err != nil { return err }
    total := 0
    for _, it := range items { total += it.Price * req.Qty[it.ID] }
    if req.CouponCode != "" {
        d, err := s.coupons.Lookup(ctx, req.CouponCode)
        if err == nil && d.Valid() { total -= d.Amount }
    }
    // ... 驗證、扣庫存、建訂單、發事件
}

★ 優點:從上到下讀完就知道發生了什麼。 對簡單流程這是最好的組織方式。
★ 缺點:規則散落在各個腳本裡,第五個使用案例會開始大量重複。

② Domain Model(領域模型)

// ★ 規則放在領域物件裡,而不是 service 裡
type Order struct {
    items    []LineItem
    status   Status
    discount Discount
}
 
func (o *Order) AddItem(p Product, qty int) error {
    if o.status != StatusDraft {
        return ErrOrderNotEditable            // ★ 不變式由物件自己守護
    }
    if qty <= 0 { return ErrInvalidQty }
    o.items = append(o.items, LineItem{p, qty})
    return nil
}
 
func (o *Order) Total() Money { /* 規則在這裡,只有一份 */ }

★ 優點:規則只有一份,且「不可能建出不合法的 Order」。
★ 缺點:要設計、要對應到資料庫(見 工具-物件關聯映射模式)、學習曲線高。

③ Table Module:一個類別對應一張表,方法操作「整個表」而非單一列。
(.NET 的 DataSet 風格,現在少見;概念上接近「repository + 靜態方法」。)

★ 貧血模型 vs 充血模型:問題不在「有沒有行為」

// 貧血:只有欄位
type Order struct { Items []Item; Status string; Total int }
 
// 規則全在 service 裡
func (s *OrderService) AddItem(o *Order, p Product, q int) {
    if o.Status != "draft" { ... }         // ★ 這個規則在這裡
}
func (s *ShippingService) Ship(o *Order) {
    if o.Status != "paid" { ... }          // ★ 另一個地方也在判斷 Status
}

★ 貧血模型的真正問題不是「沒有方法」,是「不變式沒有守護者」。
Order.Status 是公開可寫的 → 任何人都能把它改成任何值,
而「哪些轉換是合法的」這個知識散落在所有碰它的地方(見 狀態模式)。

★ 但貧血模型不是「錯」:

對簡單的 CRUD,貧血模型 + Transaction Script 是正確且更簡單的選擇。
Martin Fowler 稱它為 anti-pattern,是針對「業務複雜卻還用貧血模型」的情況。
判準是業務複雜度,不是風格偏好。

★ 怎麼選(可操作的判準)

訊號該用
主要是 CRUD + 少量驗證★ Transaction Script
同一個業務規則在 3 個以上的地方重複★ 該往 Domain Model 移動
狀態轉換有明確規則,而且被多處檢查★ Domain Model(+ 狀態模式)
規則常變、需求方講的是「業務語言」★ Domain Model(+ 工具-通用語言)
讀多寫少、主要是查詢與報表★ Transaction Script 或 CQRS 的讀側直接查

★ 最務實的路線:從 Transaction Script 開始,當規則開始重複時再把它往領域物件裡收。
不要一開始就建完整的領域模型——你那時還不知道規則長什麼樣。

★ CQRS:讀寫可以用不同的組織方式

寫側用 Domain Model(要守護不變式)、讀側直接查詢(不需要領域物件)。

// 寫:走聚合,守規則
order.AddItem(p, 3)
repo.Save(ctx, order)
 
// ★ 讀:直接一個 SQL 投影出前端要的形狀,不載入整個聚合
rows := db.Query(`SELECT o.id, o.total, c.name FROM orders o JOIN ... `)

★ 這解掉了領域模型最大的實務痛點:為了顯示一個列表而載入一堆聚合,既慢又麻煩。
兩側用不同的模型是完全正當的(見 工具-領域邏輯組織模式 的上游 工具-實體值物件與聚合)。

🧪 我實際套用的紀錄

  • 2026-07-15:(待填)

⚠️ 注意 / 什麼時候不適用

  • ★★ 不要為簡單 CRUD 套領域模型。這是 DDD 最常見的誤用——
    你會得到一堆只有 getter/setter 的「領域物件」+ 一層無意義的 mapper,複雜度全增、收益全無。
  • ★ 領域模型需要好的持久化策略。物件模型與關聯模型的阻抗不匹配是真實的痛
    (見 工具-物件關聯映射模式)。沒想清楚這一層就上領域模型,會卡在 ORM 地獄。
  • ★ 領域物件不該知道資料庫。一旦 Order.Save() 出現,它就不是領域模型了(那是 Active Record)。
  • 聚合的邊界決定了交易邊界。一次交易只改一個聚合——跨聚合要用最終一致
    (見 工具-實體值物件與聚合、工具-分散式事務的取捨)。
  • 不同的限界上下文可以用不同的模式。訂單用領域模型、報表用交易腳本——這是對的,不是不一致
    (見 工具-限界上下文)。
  • Go 的慣例比較偏 Transaction Script。Go 社群不太做深度的領域建模;
    ★ 但「把規則收進型別裡並守護不變式」的核心價值仍然適用——用未匯出欄位 + 建構函式就能做到很多。

🔗 相關工具

🎯 什麼情境該想到我

當你要設計「物件如何對應到資料庫」,或在不同 ORM 風格間抉擇、遇到 ORM 效能問題時。

⚙️ 怎麼用(認識底層模式)

  1. Active Record:物件自己包含資料庫存取邏輯(user.save())。簡單、快速,適合邏輯不複雜的 CRUD(Rails、Eloquent)。
  2. Data Mapper:物件與資料庫完全分離,由 Mapper 負責搬運。乾淨、可測、領域物件不依賴 DB,適合複雜領域(Hibernate、Prisma、TypeORM 的 mapper 風格)。
  3. Unit of Work:一次交易內追蹤所有變更,最後統一提交/回滾 → 減少 DB 往返、保證一致。
  4. Identity Map:同一物件一次請求只載入一次,避免重複查詢與不一致。

★ 根本問題:阻抗不匹配(impedance mismatch)

物件模型關聯模型
巢狀、有繼承、有參考★ 平的表格、只有外鍵
以「身分」識別(指標)★ 以「值」識別(主鍵)
沒有集合操作★ 集合導向(JOIN、GROUP BY)
一次操作一個物件★ 一次操作一批列(這是它快的原因)

★ ORM 就是在翻譯這兩者之間的落差,而所有 ORM 的痛苦都來自「翻譯有損」。

★ Active Record vs Data Mapper

// ★ Active Record:物件自己知道怎麼存
user.Email = "new@x.com"
user.Save()                      // ★ 領域物件依賴資料庫
 
// ★ Data Mapper:物件完全不知道資料庫存在
user.ChangeEmail("new@x.com")    // 純領域行為
repo.Save(ctx, user)             // ★ 由外部的 mapper 負責搬運
Active RecordData Mapper
上手★ 快慢
樣板程式★ 少多
領域物件可測性⚠️ 需要 DB 或 mock★ 純記憶體可測
適合★ CRUD、表結構≈物件結構★ 複雜領域、模型與表差很多
例子Rails、Eloquent、GORMHibernate、Prisma、SQLAlchemy(classical)

★ 選擇判準和 工具-領域邏輯組織模式 一致:
Transaction Script → Active Record;Domain Model → Data Mapper。 兩者要配對,混用會很怪。

★★ N+1 查詢:ORM 最常見也最貴的問題

orders := repo.FindAll()              // ① 1 次查詢
for _, o := range orders {
    fmt.Println(o.Customer.Name)      // ★ 每一筆都觸發一次查詢 → N 次
}
// ★ 100 筆訂單 = 101 次查詢 = 101 次網路往返

★ 它的致命之處是「在開發環境完全看不出來」(10 筆資料時很快),
上線之後資料量一大就爆。

四種修法:

方法做法
★ Eager loadingPreload("Customer") / include —— 一次 JOIN 或兩次查詢帶回
★ DataLoader(批次 + 快取)把同一輪的 N 個查詢合併成 WHERE id IN (...)。GraphQL 生態的標配
直接寫 SQL需要什麼查什麼
★ 偵測在測試或開發環境開「查詢計數斷言」——超過 N 次就讓測試失敗

★ 最後一項最重要:N+1 是「可以被自動偵測」的,不要靠人工 review。

★ 另外兩個核心模式

Unit of Work(工作單元)

uow := NewUnitOfWork(db)
uow.Register(order)                  // 追蹤變更
uow.Register(inventory)
if err := uow.Commit(ctx); err != nil { ... }   // ★ 一次交易、統一提交

★ 價值:(1) 一次交易邊界 (2) 減少往返(批次寫入)(3) 寫入順序由 UoW 決定(避免外鍵順序問題)。

Identity Map(身分映射)

同一個請求內,同一個 ID 只載入一次,回傳同一個物件實例。
★ 價值:避免重複查詢,也避免「同一筆資料在記憶體裡有兩個版本」導致的詭異 bug。

★ Go 的實務選擇(和 Java/Ruby 生態很不一樣)

工具定位
database/sql + 手寫 SQL★ Go 社群的主流。明確、可控、沒有魔法
sqlc★ 從 SQL 產生型別安全的 Go 程式碼——寫 SQL 但拿到編譯期檢查。近年最受推薦的做法
sqlxdatabase/sql 的薄封裝(掃描到 struct)
GORMActive Record 風格。方便但魔法多、效能陷阱多、N+1 容易發生
entschema-as-code + 產生程式碼,偏 Data Mapper

★ Go 社群偏好「明確的 SQL + 程式碼產生」而非「執行期的 ORM 魔法」,
理由和 Go 的整體哲學一致:可讀、可預測、沒有隱藏的成本。
sqlc 是這個哲學的代表——它讓你同時擁有 SQL 的控制力與型別安全(見 工具-型別推導還是解耦 的同源思路)。

🧪 我實際套用的紀錄

  • 2026-07-15:(待填)

⚠️ 注意 / 什麼時候不適用

  • ★★ N+1 是最常見的效能殺手(見上)。在 CI 加查詢計數斷言。
  • ★ ORM 隱藏了成本。user.Orders 這一行可能是一次 JOIN、也可能是 100 次查詢——
    從程式碼上看不出來。 → 一定要開 SQL log 看實際發出的查詢。
  • ★ 惰性載入 + 交易邊界外 = 爆炸。交易關閉後才存取 lazy 欄位 → 錯誤或額外查詢
    (Java 的 LazyInitializationException 是經典)。
  • 複雜查詢不要硬用 ORM。報表、分析、多表聚合 → 直接寫 SQL。
    ★ 「全部都用 ORM」和「全部都手寫 SQL」都是教條,混用才務實。
  • ORM 的遷移功能要小心。自動產生的 migration 可能鎖表或重寫整張表(見 工具-資料編碼與演進)。
  • 批次操作要用批次 API。迴圈裡 save() 一萬次 = 一萬次往返。
  • 不要讓 ORM 決定你的領域模型。為了 ORM 方便而把領域物件變成貧血的資料袋,是本末倒置。

🔗 相關工具

① 領域邏輯 Domain Logic

  • [[交易指令稿|交易指令稿 Transaction Script]]
  • [[領域模型|領域模型 Domain Model]]
  • [[表格模組|表格模組 Table Module]]
  • [[服務層|服務層 Service Layer]]

② 資料源架構 Data Source Architectural

  • [[表格資料閘道|表格資料閘道 Table Data Gateway]]
  • [[列資料閘道|列資料閘道 Row Data Gateway]]
  • [[主動記錄|主動記錄 Active Record]]
  • [[資料對應器|資料對應器 Data Mapper]]

③ 物件關聯行為 O-R Behavioral

  • [[工作單元|工作單元 Unit of Work]]
  • [[識別對應|識別對應 Identity Map]]
  • [[延遲載入|延遲載入 Lazy Load]]

④ 物件關聯結構 O-R Structural

  • [[識別欄位|識別欄位 Identity Field]]
  • [[外鍵對應|外鍵對應 Foreign Key Mapping]]
  • [[關聯表對應|關聯表對應 Association Table Mapping]]
  • [[從屬對應|從屬對應 Dependent Mapping]]
  • [[內嵌值|內嵌值 Embedded Value]]
  • [[序列化大物件|序列化大物件 Serialized LOB]]
  • [[單表繼承|單表繼承 Single Table Inheritance]]
  • [[類別表繼承|類別表繼承 Class Table Inheritance]]
  • [[具象表繼承|具象表繼承 Concrete Table Inheritance]]
  • [[繼承對應器|繼承對應器 Inheritance Mappers]]

⑤ 物件關聯中繼資料 O-R Metadata

  • [[中繼資料對應|中繼資料對應 Metadata Mapping]]
  • [[查詢物件|查詢物件 Query Object]]
  • [[儲存庫|儲存庫 Repository]]

⑥ Web 表現層 Web Presentation

  • [[模型-視圖-控制器|模型-視圖-控制器 Model View Controller]]
  • [[頁面控制器|頁面控制器 Page Controller]]
  • [[前端控制器|前端控制器 Front Controller]]
  • [[樣板視圖|樣板視圖 Template View]]
  • [[轉換視圖|轉換視圖 Transform View]]
  • [[兩步驟視圖|兩步驟視圖 Two Step View]]
  • [[應用控制器|應用控制器 Application Controller]]

⑦ 分散式 Distribution

  • [[遠端外觀|遠端外觀 Remote Facade]]
  • [[資料傳輸物件|資料傳輸物件 Data Transfer Object]]

⑧ 離線並發 Offline Concurrency

  • [[樂觀離線鎖|樂觀離線鎖 Optimistic Offline Lock]]
  • [[悲觀離線鎖|悲觀離線鎖 Pessimistic Offline Lock]]
  • [[粗粒度鎖|粗粒度鎖 Coarse-Grained Lock]]
  • [[隱含鎖|隱含鎖 Implicit Lock]]

⑨ 會話狀態 Session State

  • [[用戶端會話狀態|用戶端會話狀態 Client Session State]]
  • [[伺服器會話狀態|伺服器會話狀態 Server Session State]]
  • [[資料庫會話狀態|資料庫會話狀態 Database Session State]]

⑩ 基礎模式 Base Patterns

  • [[閘道|閘道 Gateway]]
  • [[對應器|對應器 Mapper]]
  • [[層超型|層超型 Layer Supertype]]
  • [[分離介面|分離介面 Separated Interface]]
  • [[登錄器|登錄器 Registry]]
  • [[值物件|值物件 Value Object]]
  • [[金錢|金錢 Money]]
  • [[特例|特例 Special Case]]
  • [[外掛|外掛 Plugin]]
  • [[服務樁|服務樁 Service Stub]]
  • [[記錄集|記錄集 Record Set]]

✨ 關鍵取捨

🧰 即用工具

① 領域邏輯(4)

② 資料源架構(4)

③ 物件關聯行為(3)

④ 物件關聯結構(10)

⑤ 物件關聯中繼資料(3)

⑥ Web 表現層(7)

⑦ 分散式(2)

⑧ 離線並發(4)

⑨ 會話狀態(3)

⑩ 基礎模式(11)

Link to original

🧰 這本書給我的工具(即用快速工具)

📖 完整型錄:51 個企業應用模式(詳解在 reference/)

① 領域邏輯 Domain Logic

② 資料源架構 Data Source Architectural

③ 物件關聯行為 O-R Behavioral

④ 物件關聯結構 O-R Structural

⑤ 物件關聯中繼資料 O-R Metadata

⑥ Web 表現層 Web Presentation

⑦ 分散式 Distribution

⑧ 離線並發 Offline Concurrency

⑨ 會話狀態 Session State

⑩ 基礎模式 Base Patterns

✨ 關鍵重點(Layer 1–2)

  • 領域邏輯三選項:Transaction Script(程序式、簡單)、Domain Model(物件化、複雜業務)、Table Module。
  • 資料源模式:Active Record(物件自己會存取 DB,簡單)vs Data Mapper(分離物件與 DB,複雜但乾淨)。
  • Unit of Work:追蹤一次交易內的變更,統一提交/回滾。
  • Identity Map:同一物件在一次請求內只載入一次,避免重複與不一致。
  • 分層:展現層 / 領域層 / 資料源層,各司其職。

💬 金句原文(Layer 0)

  • 「模式不是拿來照抄的規則,而是幫你思考取捨的起點。」

🔗 相關