📌 30 秒摘要(Layer 3)
把物件導向設計中反覆出現的問題,整理成 23 個有名字的解法(模式),分成創建型、結構型、行為型三類。但比模式本身更重要的是背後的兩條設計心法:針對介面編程,而非實作;優先使用物件組合,而非類別繼承。所有模式的本質,都是封裝變化——把會變的部分隔離起來。
🗺 心智圖(Canvas)
設計模式
Link to original 🎨 設計模式 GoF
Design Patterns — 四人幫 本質:封裝變化
🎯 什麼情境該想到我
當你在猶豫「這個參數該收具體型別還是介面」,或「測試時怎麼把資料庫/HTTP 換掉」的時候。
★ 但在 Go 裡,這條原則有一個和 Java 相反的結論——那是本卡的重點。⚙️ 怎麼用
原則本身
依賴抽象,不依賴具體實作。 呼叫端只認介面,不認是誰實作的。
三個實質好處:
- 可替換(換套件、換演算法、換供應商)
- ★ 可測試(換成測試替身,不碰網路與資料庫)
- 編譯期解耦(改實作不用重編依賴它的套件)
★ 介面該定義在哪一邊(最重要的一個判斷)
❌ Java 式:實作方定義介面,並「宣告我實作它」 package storage type Storage interface { Get(...); Put(...); List(...); Delete(...); Stat(...) } type S3 struct{} // implements Storage package app func Process(s storage.Storage) // ★ 被迫依賴一個大介面,只用得到 Get ✅ Go 式:★ 消費方定義介面,只放它真正用到的方法 package app type Getter interface { Get(ctx context.Context, k string) ([]byte, error) } // ★ 只要一個方法 func Process(g Getter) { ... } package storage type S3 struct{} // ★ 完全不知道 app.Getter 存在,但天然滿足它 func (S3) Get(...) ([]byte, error)★ Go 的隱式介面滿足讓「消費方定義介面」成為可能,而這帶來三件事:
- 介面自然會很小(你只寫你用得到的方法)
- 實作方不需要 import 消費方(依賴方向乾淨)
- 別人的型別(第三方套件)也能滿足你的介面,不用改對方程式碼
這就是 Go 諺語 “accept interfaces, return structs” 的由來。
Java 的「介面放在實作旁邊」在 Go 裡是反模式。★ 「只有一個實作」要不要抽介面
情況 抽不抽 只有一個實作,且測試不需要替換它(純計算) ❌ 不抽。直接用具體型別 只有一個實作,但測試需要替換(會打網路/資料庫/檔案/時間) ✅ 抽 ——★ 測試需求本身就是正當理由 已經有兩個以上實作 ✅ 抽 「以後可能會有第二個」 ❌ 不抽。YAGNI ——真的出現時再抽,那時才知道正確的邊界 ★ 最常見的過度設計:為每個 struct 配一個同名介面(
UserService+IUserService)。
這在 Go 裡沒有任何好處——它不會讓程式更解耦,只是多一層間接與一個要同步維護的檔案。實務:Go 的三個標準介面
io.Reader // 一個方法,全世界都能滿足 io.Writer error // ★ 只有 Error() string★ Go 標準庫最成功的抽象都只有一到兩個方法。 這不是巧合——
介面越小,能滿足它的型別越多,組合的可能性越大。(見 工具-Go介面小而隱式)TypeScript 的對應
// TS 也是結構型別,同樣「消費方定義」最好 type Getter = { get(key: string): Promise<Uint8Array | null> }; export async function process(g: Getter) { ... } // ★ 任何有 get 方法的物件都能傳進來,連 class 都不用🧪 我實際套用的紀錄
- 2026-07-14:(待填)
⚠️ 注意 / 什麼時候不適用
- ★ 介面太大 = 沒有介面。一個十個方法的介面,只有原本那一個型別滿足得了——
它沒有提供任何替換性,只是把具體型別換了個名字。- ★ 不要為了「看起來解耦」而抽。判準是**「有沒有第二個實作,或測試需不需要替換」**。
- 回傳介面通常是錯的(Go):回傳具體型別讓呼叫端拿到完整能力,要不要縮成介面由它決定。
例外:回傳的型別真的有多種(sql.Driver)、或要隱藏內部型別。- 介面會掩蓋效能特性:
io.Reader背後是記憶體還是網路,呼叫端看不出來。
效能敏感的地方要在文件裡講清楚。- 動態分派有成本(無法內聯)。極熱的迴圈上,具體型別更快——但先量再說。
- 不要把介面當文件。介面描述「能做什麼」,不描述「保證什麼」(複雜度、是否阻塞、是否並行安全)——那些要寫在註解裡。
🔗 相關工具
- 工具-Go介面小而隱式 —— Go 介面哲學的完整說明
- 工具-優先組合而非繼承 —— 組合的前置條件:被組合的東西要是介面
- 工具-封裝變化點 —— 為什麼要抽:把會變的藏在介面後面
- 工具-開閉原則 —— 目的地
- 工具-打破依賴以便測試 —— 測試替換是抽介面最常見的正當理由
- 策略模式、配接器模式、橋接模式 —— 建立在這條原則上的模式
🎯 什麼情境該想到我
當你在猶豫「用繼承還是組合來重用行為」,或繼承層級越來越深、改父類就牽連一堆子類的時候。
★ 如果你寫 Go:這不是選擇題——Go 根本沒有繼承。 但理解「為什麼」仍然重要。⚙️ 怎麼用
原則
重用行為時,先想「有一個」(has-a,組合)而非「是一個」(is-a,繼承)。
繼承 組合 綁定時機 編譯期,寫死 執行期,可換 可見性 ★ 白箱(子類看得到父類內部) 黑箱(只透過介面互動) 耦合 強(父類改動直接影響子類) 弱 多重來源 多數語言不支援多重繼承 ★ 可以組合任意多個 ★ 為什麼繼承這麼脆弱:三個具體問題
① 類別爆炸
咖啡 × {加牛奶, 加糖, 加奶泡} 用繼承 → ★ 2³ = 8 個子類 組合 → 3 個裝飾者→ 這正是 裝飾者模式 存在的理由。
② 脆弱基底類別(Fragile Base Class)
class Base { void addAll(List<T> items) { for (T i : items) add(i); // ★ 內部呼叫了自己的 add } void add(T i) { count++; } } class Counting extends Base { @Override void add(T i) { super.add(i); logged++; } @Override void addAll(List<T> l) { super.addAll(l); logged += l.size(); } } // ★ addAll 會讓 logged 被加兩次 —— 因為 Base.addAll 內部又呼叫了被覆寫的 add★ 子類必須知道父類的「內部實作細節」才能正確覆寫——這就是白箱重用的代價。
而父類的實作細節隨時可能在下一版改變。③ 語義被污染
「Stack extends Vector」是 Java 標準庫的著名錯誤——Stack 因此繼承了insertAt(),
可以從中間插入,完全違反堆疊的語義。★ Go 的情況:嵌入不是繼承
Go 用 struct 嵌入(embedding) 提供語法便利,但它是組合,不是繼承:
type Logger struct{ prefix string } func (l Logger) Log(msg string) { fmt.Println(l.prefix, msg) } type Server struct { Logger // ★ 嵌入 —— Server 直接擁有 Log 方法 addr string } s := Server{Logger{"[srv]"}, ":8080"} s.Log("started") // ★ 語法上像繼承但關鍵差異:沒有虛擬分派(virtual dispatch)
type Base struct{} func (b Base) Name() string { return "base" } func (b Base) Greet() string { return "hi, " + b.Name() } // ★ b 永遠是 Base type Child struct{ Base } func (c Child) Name() string { return "child" } Child{}.Greet() // ★ 回傳 "hi, base" —— 不是 "hi, child"★ 這是從 Java/C++ 轉 Go 最容易踩的坑。
嵌入型別的方法永遠呼叫自己的方法,不會分派到外層型別。
→ 所以 Go 裡不能用嵌入模擬 範本方法模式;要用函式欄位或介面參數(見該頁)。好處是:脆弱基底類別的問題在 Go 裡根本不存在。
Go 的嵌入該用在哪:
用途 例子 ★ 介面嵌入(組合小介面) io.ReadWriter = interface{ Reader; Writer }轉發方法,省掉樣板 嵌入 sync.Mutex讓型別直接有Lock()/Unlock()裝飾/包裝 嵌入一個介面,只覆寫其中幾個方法 // ★ 嵌入介面做部分覆寫 —— 很好用的技巧 type loggingStore struct { Store // 嵌入介面:其餘方法自動轉發 log *slog.Logger } func (s loggingStore) Put(ctx context.Context, k string, v []byte) error { s.log.Info("put", "key", k) return s.Store.Put(ctx, k, v) // ★ 只覆寫這一個 }TypeScript 的情況
TS 有
class extends,可以照 Java 那套做——但生態的主流是組合:// ❌ 繼承 class AdminUser extends User { ... } // ✅ 組合 + 型別交集 type Admin = User & { permissions: Permission[] }; // ✅ 行為用高階函式組合(就是 [[裝飾者模式]]) const withRetry = <T>(f: () => Promise<T>, n: number) => async () => { ... };🧪 我實際套用的紀錄
- 2026-07-14:(待填)
⚠️ 注意 / 什麼時候不適用
- ★ 不是「禁用繼承」。真正的 is-a、父類穩定、需要多型時,繼承仍然恰當且更簡潔。
判準:子類能不能在任何需要父類的地方替換它而不出錯(里氏替換原則)?不能 → 不該繼承。- 組合的代價是樣板程式碼:要手動轉發方法。Go 的嵌入解掉了大部分,其他語言可能要寫一堆 delegate。
- ★ 組合也會被濫用:十層包裝的物件,除錯時堆疊裡全是 wrapper,和深繼承一樣難讀。
- ★ Go 的嵌入有名稱衝突規則:兩個嵌入型別有同名方法 → 呼叫時編譯錯誤(除非明確指定)。
外層自己定義的方法會遮蔽嵌入的。- 嵌入會暴露被嵌入型別的所有匯出方法——包含你不想暴露的。要控制就別嵌入,改用具名欄位。
sync.Mutex嵌入要注意:它會讓Lock()/Unlock()變成公開 API,外部可以鎖住你的物件。通常該用具名的私有欄位。🔗 相關工具
🎯 什麼情境該想到我
當你「知道該用組合、該抽介面,但不知道該抽哪裡」的時候。
★ 這是所有設計模式背後的那一條原則——GoF 二十三個模式都是它的具體化。
而它的難處不在「怎麼封裝」,在**「找對變化點」**。⚙️ 怎麼用
原則
找出會變動的部分,把它與不變的部分分開,並封裝起來。
會變的 → 抽成可替換的元件(介面 + 實作) 不變的 → 保持簡單,依賴抽象而非具體GoF 二十三個模式其實都在回答「封裝什麼」:
封裝什麼 模式 演算法 策略模式 物件的建立 工廠方法模式、抽象工廠模式、建造者模式 額外的行為 裝飾者模式 狀態相依的行為 狀態模式 對結構的操作 訪問者模式 請求本身 命令模式 物件之間的互動 中介者模式 走訪方式 迭代器模式 兩個獨立變化的維度 橋接模式 ★ 記住這張表比背二十三個模式有用:
遇到問題時問「這裡會變的是什麼?」,答案會直接指向對應的模式。★ 怎麼找出變化點(這才是難的部分)
不要用猜的。用證據:
證據來源 怎麼看 ★ git 歷史 git log --format=format: --name-only | sort | uniq -c | sort -rn | head -20
★ 過去一年改最多次的檔案,就是你的變化點——這是最可靠的證據需求的措辭 「目前支援 A,未來還會有 B、C」← 明確的變化軸 ★ 條件分支的成長 同一個 switch在三次 commit 裡各加了一個 case → 它就是變化點重複的形狀 三個函式長得幾乎一樣,只差中間幾行 領域知識 稅率、匯率、法規、費率本來就會變;資料結構的核心運算通常不會 ★
git log那一招值得記起來。 它把「我覺得這裡會變」變成「這裡過去一年改了 47 次」。
(見 工具-影響分析、工具-破窗理論與軟體熵)★ 一個具體的判斷流程
① 這個需求變動,需要改幾個地方? 1 個 → 沒問題,不需要封裝 多個且分散 → ★ 有變化點沒被封裝 ② 這些地方為什麼會一起改? → 它們共享的那個「概念」就是該被封裝的東西 ③ 把那個概念抽出來,讓其他部分依賴它的介面反例(抽錯地方):
現象:每加一種報表就要改三個檔案 ❌ 誤判:「報表產生器」是變化點 → 抽一個 ReportGenerator 介面 結果:三個檔案還是都要改,因為真正變的是「資料來源 × 輸出格式」兩個維度 ✅ 正解:兩個維度各自封裝(見 [[橋接模式]])🧪 我實際套用的紀錄
- 2026-07-14:(待填)
⚠️ 注意 / 什麼時候不適用
- ★ 猜錯變化點比不封裝更糟。你付了抽象的代價(多一層間接、更難讀),
卻沒有得到彈性——而且錯誤的抽象會抵抗正確的改動。- ★ 等證據出現再封裝。第一次寫死、第二次容忍重複、第三次才抽象。
Sandi Metz:“duplication is far cheaper than the wrong abstraction.”(見 工具-DRY原則)- 不是所有變動都該被封裝。有些改動就是該改程式碼——
封裝的目的是「讓高頻改動變安全」,不是「讓所有改動都不用改程式」。- 封裝有成本:多一個介面、多一層間接、多一個「誰決定用哪個」的問題、profiling 變難。
變化頻率低的地方不值得付。- ★ 過度封裝的症狀:每個 struct 都有一個同名介面、追一個呼叫要跳五層、
改一行要看三個檔案——這時候「彈性」已經變成「難以理解」。- 維度要分清楚。兩個獨立變化的維度硬塞進一個抽象 → 類別爆炸(見 橋接模式)。
🔗 相關工具
🧭 設計心法(先於模式)
🏗 創建型 Creational(5) 管「怎麼建立物件」 [[抽象工廠模式]]・[[建造者模式]]・[[工廠方法模式]]・[[原型模式]]・[[單例模式]]
🧩 結構型 Structural(7) 管「怎麼組合物件與類別」 [[配接器模式]]・[[橋接模式]]・[[組合模式]]・[[裝飾者模式]]・[[外觀模式]]・[[享元模式]]・[[代理模式]]
🔄 行為型 Behavioral(11) 管「物件間怎麼分工與溝通」 [[責任鏈模式]]・[[命令模式]]・[[直譯器模式]]・[[迭代器模式]]・[[中介者模式]]・[[備忘錄模式]]・[[觀察者模式]]・[[狀態模式]]・[[策略模式]]・[[範本方法模式]]・[[訪問者模式]]
📖 模式型錄(23)
[[策略模式|Strategy]] vs [[狀態模式|State]] 選演算法 vs 隨狀態換行為
[[裝飾者模式|Decorator]] vs [[代理模式|Proxy]] 加功能 vs 控制存取
[[配接器模式|Adapter]] vs [[橋接模式|Bridge]] 事後轉介面 vs 事前分離抽象與實作
⚖️ 容易混淆的孿生
🎯 什麼情境該想到我
當你發現「每次加一個新類型,都要回去改同一個
switch/if鏈」的時候。
★ 但這條原則最常被誤用成「什麼都預先抽象」——所以本卡有一半在講什麼時候不該套用。⚙️ 怎麼用
原則
對擴充開放,對修改關閉:新增行為靠「加新程式」,而不是「改舊程式」。
為什麼重要:改舊程式碼會重新引入風險(要重測、可能破壞既有行為);加新程式碼不動既有路徑。
三種實現手法(由輕到重)
① 資料驅動(最輕,常被忽略)
// ❌ 加一種幣別就要改函式 func Symbol(cur string) string { switch cur { case "TWD": return "NT$" case "USD": return "$" } } // ✅ 改成查表 —— 加幣別只是加一筆資料 var symbols = map[string]string{"TWD": "NT$", "USD": "$"}★ 很多「需要開閉原則」的場景,其實只是需要一張表。 先試這個,不要急著抽介面。
② 策略/介面(有行為差異時)
type PaymentMethod interface { Charge(ctx context.Context, amt Money) (*Receipt, error) } // 結帳流程只認介面,加一種付款方式不動它 func Checkout(ctx context.Context, m PaymentMethod, amt Money) error { ... }③ ★ 註冊表(真正做到「加檔案就好」)
var registry = map[string]func(Config) (PaymentMethod, error){} func Register(kind string, f func(Config) (PaymentMethod, error)) { registry[kind] = f } // ── 新檔案 linepay.go,完全不動既有程式 ── func init() { Register("linepay", newLinePay) }★ 這是
database/sql的做法(sql.Register+sql.Open),也是 Go 生態最常見的插件機制。
→ 見 工廠方法模式★ 判準:什麼會變?
開閉原則不可能對「所有可能的變化」都成立。
Bertrand Meyer 的原意是:針對「你預期會變的那一個軸」設計擴充點,其他軸保持簡單。付款方式會變 → 抽 PaymentMethod 介面 ✅ 但「結帳流程的步驟順序」不會變 → 寫死就好 ✅猜錯軸的代價:你為「付款方式」做了漂亮的擴充點,結果變的是「結帳流程」——
抽象不但沒幫上忙,還讓改動更難。🧪 我實際套用的紀錄
- 2026-07-14:(待填)
⚠️ 注意 / 什麼時候不適用
- ★ 最常見的誤用:預先抽象。
「以後可能會有第二種」不是理由。等第二種真的出現再抽——
那時你才知道兩者的共同點在哪,抽象的邊界才會正確。
→ 第一次寫死、第二次容忍重複、第三次才抽象(Rule of Three)。- ★ 錯誤的抽象比重複更貴(見 工具-DRY原則)。
抽錯的介面會讓每個新實作都要「塞進不合身的形狀」,最後每個實作都有一半方法回ErrNotSupported。- ★ 開閉原則對「加型別」開放,但對「加操作」封閉——這是它的對偶代價。
用PaymentMethod介面後,加一個新方法要改所有實作。
這就是表達式問題(見 訪問者模式):你只能選一個軸。- Go 的
switch不一定要消除。如果分支是閉集合且穩定(星期幾、HTTP 方法),
switch比介面更清楚也更快。抽介面要有理由,不是反射動作。- 註冊表的代價:
init()有副作用、初始化順序不可控、_空白匯入讓依賴變隱晦。
中小型專案直接用switch通常更好維護。- 這條原則不能單獨用。它的前提是 工具-針對介面編程 與 工具-優先組合而非繼承。
🔗 相關工具
🎯 什麼情境該想到我
當你「業務邏輯被資料庫/框架/第三方 SDK 綁死,換掉就要重寫」的時候,
或「想理解為什麼 Clean Architecture 要畫那些同心圓」的時候。⚙️ 怎麼用
兩條原則,一體兩面
① 依賴反轉原則(DIP) —— 關於依賴方向
高層模組不應依賴低層模組,兩者都應依賴抽象。抽象不應依賴細節,細節應依賴抽象。
② 好萊塢原則 —— 關於控制流方向
“Don’t call us, we’ll call you.” 由高層決定何時呼叫低層,低層只實作被回呼的介面。
★ 「反轉」到底反轉了什麼
❌ 傳統:控制流與依賴方向「一致」 OrderService ──呼叫且 import──▶ PostgresRepo ★ 業務邏輯依賴資料庫實作 → 換資料庫要改業務邏輯 ✅ 反轉後:控制流不變,★ 依賴方向被反轉 OrderService ──呼叫──▶ OrderRepository(介面,★ 定義在業務層) ▲ │ 實作(依賴方向往上) PostgresRepo(基礎設施層)★ 關鍵:介面「屬於」高層,不屬於低層。
OrderRepository這個介面和OrderService放在同一個套件——
它表達的是「業務邏輯需要什麼」,而不是「資料庫能提供什麼」。這樣一來,基礎設施依賴業務邏輯,而不是反過來——依賴方向被「反轉」了。
這正是 Clean Architecture / 六角架構那些同心圓的全部內容。★ Go 的長相
// ── 業務層(不 import 任何基礎設施)── package order // ★ 介面定義在這裡,而且只有業務邏輯真正需要的方法 type Repository interface { Save(ctx context.Context, o *Order) error FindByID(ctx context.Context, id string) (*Order, error) } type Service struct{ repo Repository } // ★ 依賴抽象 func (s *Service) Place(ctx context.Context, req Request) (*Order, error) { o := newOrder(req) // 業務規則 if err := o.Validate(); err != nil { return nil, err } return o, s.repo.Save(ctx, o) }// ── 基礎設施層(import 業務層)── package postgres type OrderRepo struct{ db *sql.DB } func (r *OrderRepo) Save(ctx context.Context, o *order.Order) error { ... } // ★ 不需要寫 "implements order.Repository" —— Go 的隱式滿足// ── main:唯一知道所有具體型別的地方 ── func main() { db, _ := sql.Open("postgres", dsn) svc := order.NewService(&postgres.OrderRepo{DB: db}) // ★ 組裝 }★
order套件完全不 importpostgres——
這可以用go list -deps或架構測試(如go-arch-lint)自動驗證,
讓「架構規則」從文件變成 CI 檢查。★ 依賴反轉 vs 依賴注入(常被混淆)
依賴反轉(DIP) 依賴注入(DI) 是什麼 ★ 原則:依賴方向該指向抽象 ★ 手法:從外部把相依傳進來 回答 「該依賴什麼」 「怎麼拿到它」 沒有對方行嗎 可以只用 DI 不做 DIP(注入一個具體型別) 可以只做 DIP 不用 DI(用 service locator,但不建議) ★ 一句話:DIP 決定「依賴誰」,DI 決定「怎麼拿到」。 兩者常一起出現,但不是同一件事。
注入一個具體的*postgres.OrderRepo是 DI 但不是 DIP。★ 好萊塢原則:函式庫 vs 框架
函式庫(library):★ 你呼叫它 → 你控制流程 result := json.Marshal(v) 框架(framework):★ 它呼叫你 → 它控制流程(控制反轉,IoC) http.HandleFunc("/x", myHandler) // 你只是註冊,何時執行由框架決定「控制反轉」的其他常見形態:
形態 例子 回呼/事件處理器 http.Handler、EventEmitter範本方法模式 父類控制骨架,子類填空 觀察者模式 Subject 決定何時通知 middleware 框架決定何時呼叫 next生命週期 hook init()、React 的useEffect🧪 我實際套用的紀錄
- 2026-07-14:(待填)
⚠️ 注意 / 什麼時候不適用
- ★ 介面放錯邊是最常見的錯誤。把
Repository介面放在postgres套件裡 →
業務邏輯還是要 import 基礎設施,完全沒有反轉。 介面必須屬於使用它的那一方
(見 工具-針對介面編程)。- ★ 介面不要照著資料庫設計。
FindByUserIDAndStatusOrderByCreatedAt這種方法名
代表你在用 ORM 的語言描述業務需求——那是依賴沒有真正反轉的訊號。- ★ 不是每一層都要反轉。小專案、CRUD、一次性腳本直接用具體型別更快更清楚。
DIP 的成本(多一個介面、多一層間接、追程式要跳)在簡單系統裡不划算。- ★ 抽象洩漏:介面上出現
sql.Tx、*gorm.DB、context.Context裡塞資料庫交易 →
抽象已經洩漏了。事務邊界的處理是這裡最難的部分(常見解法是 Unit of Work,見 工具-領域邏輯組織模式)。- DI 框架不是必需品。Go 社群多半在
main()裡手動組裝就好;
相依真的很多時才考慮wire(編譯期產生程式碼,比反射式的 DI 容器好除錯)。- 好萊塢原則讓控制流難追。「誰呼叫了我」在框架裡不明顯——這是控制反轉的固有代價。
🔗 相關工具
- 工具-針對介面編程 —— ★ 介面該定義在哪一邊
- 工具-封裝變化點 —— 先找出該被反轉的是什麼
- 工具-優先組合而非繼承 —— 注入的前提
- 範本方法模式、觀察者模式 —— 好萊塢原則的具體模式
- 工具-服務邊界與演進式拆分 —— 依賴方向決定了邊界能不能切
- 工具-打破依賴以便測試 —— 反轉之後測試才好寫
- 工具-領域邏輯組織模式 —— 事務邊界的處理
🧰 這本書給我的工具
設計心法(先於模式)
- 工具-針對介面編程 — 想降低耦合、讓實作可替換時
- 工具-優先組合而非繼承 — 在繼承與組合間抉擇時
- 工具-封裝變化點 — 設計時想預留變化彈性
- 工具-開閉原則 — 想讓新功能用「加的」而不是「改的」時
- 工具-好萊塢原則與依賴反轉 — 想把業務邏輯與框架/資料庫解耦時
★ 型錄各頁都寫了「這個模式在 Go 與 TypeScript 裡實際長什麼樣」——
GoF 是 1994 年的 C++/Smalltalk 書,而 Go 沒有繼承、TS 有判別聯集與結構型別,
有些模式因此變形(Builder → Functional Options),有些直接消失(範本方法、訪問者)。
🏗 創建型 Creational(5,管「怎麼建立物件」)
🧩 結構型 Structural(7,管「怎麼組合物件與類別」)
🔄 行為型 Behavioral(11,管「物件間怎麼分工與溝通」)
- 責任鏈模式 Chain of Responsibility、命令模式 Command、直譯器模式 Interpreter、迭代器模式 Iterator、中介者模式 Mediator、備忘錄模式 Memento、觀察者模式 Observer、狀態模式 State、策略模式 Strategy、範本方法模式 Template Method、訪問者模式 Visitor
✨ 關鍵重點(Layer 1–2)
- 三大類共 23 模式:創建型 5(Abstract Factory、Builder、Factory Method、Prototype、Singleton)、結構型 7(Adapter、Bridge、Composite、Decorator、Facade、Flyweight、Proxy)、行為型 11(Chain of Responsibility、Command、Interpreter、Iterator、Mediator、Memento、Observer、State、Strategy、Template Method、Visitor)。
- 兩條核心原則:針對介面編程、組合優於繼承。
- 共同本質:封裝變化——找出「會變的部分」把它隔離。
- 模式是「詞彙」:讓團隊用一個詞就溝通一整套設計。
- 幾組容易混淆的孿生:Strategy vs State(選演算法 vs 隨狀態換行為)、Decorator vs Proxy(加功能 vs 控制存取)、Adapter vs Bridge(事後轉介面 vs 事前分離抽象與實作)。
💬 金句原文(Layer 0)
- 「針對介面編程,而不是針對實作編程。」
- 「優先使用物件組合,而非類別繼承。」
🔗 相關
- 更好上手的教學版見 HeadFirst 設計模式(同樣的模式 + 設計原則)