📌 30 秒摘要(Layer 3)

把物件導向設計中反覆出現的問題,整理成 23 個有名字的解法(模式),分成創建型、結構型、行為型三類。但比模式本身更重要的是背後的兩條設計心法:針對介面編程,而非實作;優先使用物件組合,而非類別繼承。所有模式的本質,都是封裝變化——把會變的部分隔離起來。

🗺 心智圖(Canvas)

設計模式

🎨 設計模式 GoF

Design Patterns — 四人幫 本質:封裝變化

🎯 什麼情境該想到我

當你在猶豫「這個參數該收具體型別還是介面」,或「測試時怎麼把資料庫/HTTP 換掉」的時候。
★ 但在 Go 裡,這條原則有一個和 Java 相反的結論——那是本卡的重點。

⚙️ 怎麼用

原則本身

依賴抽象,不依賴具體實作。 呼叫端只認介面,不認是誰實作的。

三個實質好處:

  1. 可替換(換套件、換演算法、換供應商)
  2. ★ 可測試(換成測試替身,不碰網路與資料庫)
  3. 編譯期解耦(改實作不用重編依賴它的套件)

★ 介面該定義在哪一邊(最重要的一個判斷)

❌ 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 的隱式介面滿足讓「消費方定義介面」成為可能,而這帶來三件事:

  1. 介面自然會很小(你只寫你用得到的方法)
  2. 實作方不需要 import 消費方(依賴方向乾淨)
  3. 別人的型別(第三方套件)也能滿足你的介面,不用改對方程式碼

這就是 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 根本沒有繼承。 但理解「為什麼」仍然重要。

⚙️ 怎麼用

原則

重用行為時,先想「有一個」(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 套件完全不 import postgres——
這可以用 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
生命週期 hookinit()、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 容器好除錯)。
  • 好萊塢原則讓控制流難追。「誰呼叫了我」在框架裡不明顯——這是控制反轉的固有代價。

🔗 相關工具

Link to original

🧰 這本書給我的工具

設計心法(先於模式)

★ 型錄各頁都寫了「這個模式在 Go 與 TypeScript 裡實際長什麼樣」——
GoF 是 1994 年的 C++/Smalltalk 書,而 Go 沒有繼承、TS 有判別聯集與結構型別,
有些模式因此變形(Builder → Functional Options),有些直接消失(範本方法、訪問者)。

🏗 創建型 Creational(5,管「怎麼建立物件」)

🧩 結構型 Structural(7,管「怎麼組合物件與類別」)

🔄 行為型 Behavioral(11,管「物件間怎麼分工與溝通」)

✨ 關鍵重點(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)

  • 「針對介面編程,而不是針對實作編程。」
  • 「優先使用物件組合,而非類別繼承。」

🔗 相關