iThome 鐵人賽
設計模式
Builder
Creational Patterns
Codetopia
午夜十二點,Codetopia 市府發布「舊城區緊急都更案」,都更署的專案經理 Rex |Director 正對著施工藍圖,眉頭深鎖。
挑戰在於:施工流程固定(地基→鋼骨→外牆→水電→驗收),但成品卻需依街區需求,產出截然不同的小宅版與塔樓版。過去那種「一個函式蓋到底」的作法,任何微調都會引爆測試災難。
Rex 的思緒飄回了昨天。他想起 Day 4 的「供應商聯盟 (Abstract Factory)」,那招「一次換全套」很漂亮,但今天不適用。他需要的不是換掉整個施工隊,而是**「同一套分步流程」,但能彈性地對應不同建造工法,最終產出迥異的成品。**
他要的,是一種能將「施工順序」與「每一步作法」徹底解耦的智慧。
🧭 術語卡(今日會用到)
Builder:一種創生模式,旨在將複雜物件的「建造步驟」與其「最終成品表示」分離,讓同一套建造流程能創建不同產品。
Director:指導者角色,負責掌控建造的順序與流程(施工藍圖),不關心細節。
Pipes-and-Filters / Pipeline:一種中觀架構模式,將大流程拆解成一系列可組合的「濾器」節點。
常見誤用:許多人會把 Builder 當成「多參數建構子」的語法糖。但 Builder 的核心價值在於分步建構與流程/表示分離,而不僅僅是鏈式調用。
在 Rex 找到新方法前,舊的施工現場簡直是一場災難,充滿了各種「壞味道」:
一鍋燴施工法 🍲:一個 buildBuilding(...)
巨石函式,塞滿 if-else
。想加新材料?恭喜,準備再加 10 個分支判斷,然後祈禱不要改壞既有邏輯。
半成品外洩事故 💧:外部服務可以直接操作施工中的 currentWalls[]
陣列。結果就是,隔壁團隊為了修 Bug,順手就把你蓋到一半的牆給拆了。
流程硬寫在水泥裡 🧱:將「蓋塔樓要多打一遍地基」這種流程邏輯,硬焊在 UI 按鈕事件裡。導致就算換了施工隊(Builder),流程也改不掉。
永無寧日的測試地獄 🔥:同一套消防驗收腳本,在小宅和塔樓版下結果居然不一樣!只因建造流程和實作緊緊糾纏,引發了無法預期的副作用。
說穿了,這一切混亂的根源就是:「蓋房子的順序」 和 「每一步怎麼蓋」 這兩件事,被粗暴地揉在了一起。
為了解決這場施工災難,Rex 採用了今天的王牌工法——建造者模式 (Builder Pattern)。
它的核心思想是:
將一個複雜物件的建構過程與其表示分離,使得同樣的建構過程可以創建不同的表示。
用都更署的行話來說,就是確立兩大角色的神聖分工:
總監 (Director):Rex 擔任此角。他手握施工藍圖,只負責下達指令的順序,完全不碰鋼筋水泥。更重要的是,由他封裝完整的建造流程並負責最終交付。
施工隊 (Builder):Leo(小宅隊)和 Nora(塔樓隊)是實作專家。他們負責詮釋總監的指令。當 Rex 喊「上鋼骨」,Leo 隊會用輕型鋼材,而 Nora 隊則用高強度合金鋼。
最終,施工隊會交出一個完整且封存(不可變)的建築成品 (Building)。在這之後,誰都不能再對它敲敲打打。
✅ 當產物結構複雜,且建立過程有固定的步驟時。
✅ 當同一套建造流程,需要產出多種不同表現(變體)時。
✅ 當你希望同一套驗收劇本,能覆蓋所有產品變體時。
✅ 當你需要一個「階段性」的建造過程,並在最後產出一個不可變的物件時。
⛔ 如果物件只是構造函數參數多,但沒有固定的建造步驟:用命名參數 (Named Parameters) 或 DTO 就夠了。
⛔ 如果建造步驟變化極大,需要動態組合:更適合升級為管線模式 (Pipeline)。
設計模式小教室:Builder vs. Abstract Factory (Day 4)
情境 Abstract Factory Builder |一次拿到成套、風格一致的零件家族|✅|△|
|遵循固定流程、分步驟建構複雜成品|△|✅|
|同一驗收腳本覆蓋多種產品變體|△|✅|
導播,鏡頭拉一下!讓我們用三種不同的焦段,來看看這個分工合作的施工現場。
視角 | 觀念/模式 | 在 Codetopia 的說法 |
---|---|---|
微觀 (GoF) | Builder + Director | Rex (Director) 依序呼叫建造步驟,並從 Builder 手中取得最終成品。Leo (HouseBuilder) 和 Nora (TowerBuilder) 各自實現這些步驟。 |
中觀 (EIP/EDA/Actor) | Pipes-and-Filters / Pipeline | 將「打地基→鋼骨→…」看作一條訊息處理管線。每一步的完成觸發 StepCompleted 事件,交給下一個節點。 |
宏觀 (MAS) | 任務分解與協作 | 規劃代理 (PlanningAgent) 將任務分解,派發給施工代理 (ConstructionAgent) 和驗收代理 (InspectionAgent)。Director 只做排程與監控。 |
讀圖口訣:「Director 定序並交付,Builder 埋頭定工法」。
讀圖口訣:「流程只有一條主幹;不同工法在步驟內實現」。
讓我們用 Python 腳本來模擬這場職責分明、產出安全且具備可觀測性的施工秀。
from __future__ import annotations
from abc import ABC, abstractmethod
from typing import Callable, Optional
from dataclasses import dataclass
# --- 1. 產品本身:一個不可變且易於閱讀的安置大樓 ---
@dataclass(frozen=True)
class Building:
parts: tuple[str, ...]
def __str__(self) -> str:
return " | ".join(self.parts)
# --- 2. 施工隊的「契約」 (Builder Interface) ---
class IBuilder(ABC):
"""
Builder 契約:
- get_product() 交付一個不可變的產品,並且會重置 Builder 的內部狀態。
- 這意味著 get_product() 不是冪等的 (idempotent),連續呼叫會得到空的產品(因交付後已重置)。
- Builder 的實例通常不是執行緒安全的。
"""
@abstractmethod
def reset(self) -> None: ...
@abstractmethod
def build_foundation(self) -> None: ...
@abstractmethod
def build_frame(self) -> None: ...
@abstractmethod
def build_walls(self) -> None: ...
@abstractmethod
def build_mep(self) -> None: ...
@abstractmethod
def get_product(self) -> Building: ...
# --- 3. 具體的施工隊:小宅隊 & 塔樓隊 ---
class HouseBuilder(IBuilder):
def __init__(self) -> None:
self._parts: list[str] = []
def reset(self) -> None:
self._parts = []
def build_foundation(self) -> None: self._parts.append("溫馨小宅的地基")
def build_frame(self) -> None: self._parts.append("5層樓輕鋼骨")
def build_walls(self) -> None: self._parts.append("磚造外牆")
def build_mep(self) -> None: self._parts.append("家用級水電系統")
def get_product(self) -> Building:
product = Building(parts=tuple(self._parts))
self.reset()
return product
class TowerBuilder(IBuilder):
def __init__(self) -> None:
self._parts: list[str] = []
def reset(self) -> None:
self._parts = []
def build_foundation(self) -> None: self._parts.append("摩天塔樓的深層地基")
def build_frame(self) -> None: self._parts.append("30層樓高強度鋼骨")
def build_walls(self) -> None: self._parts.append("玻璃帷幕外牆")
def build_mep(self) -> None: self._parts.append("商用級水電系統")
def get_product(self) -> Building:
product = Building(parts=tuple(self._parts))
self.reset()
return product
# --- 4. 總監 (Director),負責指揮、監控並交付最終成品 ---
class Director:
def __init__(self, builder: IBuilder, on_step: Optional[Callable[[str], None]] = None):
self._builder = builder
self._on_step = on_step or (lambda step: None)
def change_builder(self, builder: IBuilder) -> None:
self._builder = builder
def _execute_and_notify(self, step_name: str, step_index: int, step_action: Callable[[], None]):
"""執行一個步驟並觸發回調"""
step_action()
self._on_step(f"{step_index}:{step_name}")
def build_minimal_viable_product(self) -> Building:
self._builder.reset()
self._execute_and_notify("foundation", 1, self._builder.build_foundation)
return self._builder.get_product()
def build_full_featured_product(self) -> Building:
self._builder.reset()
steps = [
("foundation", self._builder.build_foundation),
("frame", self._builder.build_frame),
("walls", self._builder.build_walls),
("mep", self._builder.build_mep),
]
for i, (name, action) in enumerate(steps, start=1):
self._execute_and_notify(name, i, action)
return self._builder.get_product()
# --- 5. 驗收時間 ---
def run_validation_suite(building: Building):
"""同一套驗收腳本,不管產品是什麼,都要通過!"""
print(f"\n[驗收開始] 正在檢測...")
print(f" - 產品結構: {building}")
assert building.parts, "驗收失敗:產出為空,請檢查 Builder 是否遺漏步驟"
print(f"[驗收通過] ✅ 該建築符合基本規範!")
if __name__ == "__main__":
events = []
# Rex 的指揮中心,現在有了事件監控儀表板
director = Director(HouseBuilder(), on_step=lambda s: events.append(f"StepCompleted:{s}"))
print("--- Rex 下令:建造 A 區的小宅版!---")
small_house = director.build_full_featured_product()
run_validation_suite(small_house)
print(f"施工事件紀錄: {events}")
print("\n" + "="*40 + "\n")
events.clear() # 清空上次的事件紀錄
print("--- Rex 下令:更換施工隊,建造 B 區的塔樓版!---")
director.change_builder(TowerBuilder())
skyscraper = director.build_full_featured_product()
run_validation_suite(skyscraper)
print(f"施工事件紀錄: {events}")
一個好的架構,必須是高度可測試的。Builder 模式的職責分離特性,讓測試變得非常清晰。
測試 Director(流程正確性):我們不關心產出物,只關心 Director 是否按照正確的順序呼叫了 Builder 的方法。這時可以使用一個假的 Builder (Fake/Mock)。
# (使用 pytest 風格)
def test_director_build_sequence_is_correct():
class FakeBuilder(IBuilder): # 一個只記錄呼叫順序的假 Builder
def __init__(self): self.calls = []
def reset(self): self.calls.append("reset")
def build_foundation(self): self.calls.append("foundation")
def build_frame(self): self.calls.append("frame")
def build_walls(self): self.calls.append("walls")
def build_mep(self): self.calls.append("mep")
def get_product(self): return Building(parts=("fake",))
fake_builder = FakeBuilder()
director = Director(fake_builder)
director.build_full_featured_product()
# 斷言必須嚴格檢查順序與次數
expected_sequence = ["reset", "foundation", "frame", "walls", "mep"]
assert fake_builder.calls == expected_sequence
測試 Builder(產出契約正確性):我們需要確保每個具體的 Builder 都能產出符合契約的產品。這可以使用參數化測試,讓同一套驗收邏輯跑在所有 Builder 上。
# (使用 pytest 風格)
import pytest
def meets_building_code(building: Building) -> bool:
# 語義檢查:只要有地基,且至少有4個部分,就符合基本法規
# 這種寫法比檢查 building.parts[0] 更具韌性
has_foundation = any("地基" in p for p in building.parts)
return has_foundation and len(building.parts) >= 4
@pytest.mark.parametrize("builder_class", [HouseBuilder, TowerBuilder])
def test_builder_product_contract(builder_class):
builder = builder_class()
director = Director(builder)
product = director.build_full_featured_product()
assert isinstance(product, Building)
assert meets_building_code(product), f"{builder_class.__name__} 未能產出符合法規的建築!"
想加入都更署的王牌施工隊嗎?先來挑戰這幾個任務吧!
流程的彈性:假設遇到雨天,需要跳過「外牆上漆」這個可選步驟。你認為這個判斷邏輯(if is_raining: skip_painting()
)應該放在 Director
還是 Builder
裡?寫下你的選擇與取捨的理由。(提示:「決定流程」是誰的職責?「如何實作」又是誰的職責?)
反模式紅旗 🚩:
Director 洩漏對具體工法的認知:如果在 Director
的程式碼中出現 if isinstance(self._builder, TowerBuilder): self._builder.add_gargoyle()
,這嚴重違反了開放封閉原則,因為每當新增一種 Builder,你都可能需要回來修改 Director。
Builder 產出可變物件:如果 get_product()
回傳的 Building
物件,在交付後還能被外部修改(例如 building.parts.append("違建")
),這就破壞了 Builder 模式「一次性安全建構」的核心承諾。
誤解 get_product()
的語義:連續呼叫 builder.get_product()
,第二次卻拿到一個空的產品,代表呼叫者不了解其「交付即重置」的契約。這在介面註解中必須說清楚。
今天我們在微觀層面用 Builder 模式精準分工。如果將這個「分步建構」的概念放大到整個城市架構:
在 EIP/EDA (事件驅動) 的世界裡,這條施工流程就像一個訊息管線 (Pipeline)。每一步都是一個濾器 (Filter)。當需要回滾或重試時,可以引入 Saga 模式來管理長交易的補償步驟。例如,為 Builder 的每個步驟都定義一個對應的補償函式,並以正向步驟的反序來執行補償,確保交易的一致性。
在 MAS (多代理系統) 的宏觀視角下,Director
更像是一個輕量級的排程與監控代理。它不直接呼叫方法,而是向城市的「黃頁服務 (DF)」查詢,有哪些代理宣告了「打地基」或「拉水電」的能力,然後將任務派發出去,並監控服務水準協議 (SLA)。這樣一來,要替換掉某個承包商,只需更新黃頁上的註冊資訊,完全不用修改排程邏輯。
流程固定表現可換;Director 定序、Builder 定工法,完工物件一次封存。
今天,我們見證了都更署如何運用 Builder 模式,優雅地解決了複雜物件的建造難題。
然而,有時候我們需要的不是從零開始一步步蓋,而是快速地複製一個現有的「標準戶型」,再做點個性化微調。
明天,我們將拜訪城市的「樣板局」。敬請期待 Day 6《Prototype》—— 樣板局出動:複製藍本、按需微調,見證「複製勝於創建」的時刻!
為了確保在不支援 Mermaid 渲染的環境中也能正常閱讀,以下提供文中圖表的 ASCII 替代版本:
👨💼 Director
╔═══════════════════════╗
║ - builder: IBuilder ║
║ + change_builder() ║
║ + build_full_feat...()║
║ + build_minimal...() ║
╚═══════════╤═══════════╝
│ uses
▼
📋 <<Interface>>
IBuilder
╔═══════════════════════╗
║ + reset() ║
║ + build_foundation() ║
║ + build_frame() ║
║ + build_walls() ║
║ + build_mep() ║
║ + get_product() ║
╚═══════════╤═══════════╝
│
┌───────────┴───────────┐
▼ ▼
🏠 HouseBuilder 🏢 TowerBuilder
╔═══════════════════╗ ╔═══════════════════╗
║ - _parts: list ║ ║ - _parts: list ║
║ + get_product() ║ ║ + get_product() ║
╚═══════════════════╝ ╚═══════════════════╝
│ │
│ creates │ creates
▼ ▼
🏛️ Building
╔═══════════════════════╗
║ + parts: tuple[str,...]║
║ 🔒 (不可變物件) ║
╚═══════════════════════╝
🎬 開始
│
▼
╔═══════════════╗
║ 🔄 reset() ║
╚═══════╤═══════╝
│
▼
╔═══════════════════╗
║ 🏗️ build_foundation║
╚═══════════╤═══════╝
│
▼
╔═══════════════════╗
║ 🏭 build_frame() ║
╚═══════════╤═══════╝
│
▼
╔═══════════════════╗
║ 🧱 build_walls() ║
╚═══════════╤═══════╝
│
▼
╔═══════════════════╗
║ ⚡ build_mep() ║
╚═══════════╤═══════╝
│
▼
╔═══════════════════╗
║ 📦 get_product() ║
╚═══════════╤═══════╝
│
▼
╔═══════════════╗
║ ✅ 交付成品 ║
╚═══════════════╝
┌─────────────────────── 💡 設計智慧 ───────────────────────┐
│ │
│ 🎯 Director 控制整個流程序列 │
│ 🔧 同一流程步驟,不同 Builder 有不同實作: │
│ │
│ 🏗️ build_foundation: │
│ 🏠 小宅隊 → 淺地基 │
│ 🏢 塔樓隊 → 深地基 │
│ │
│ 🏭 build_frame: │
│ 🏠 小宅隊 → 輕鋼架 │
│ 🏢 塔樓隊 → 高強度鋼骨 │
│ │
│ 🧱 build_walls: │
│ 🏠 小宅隊 → 磚造外牆 │
│ 🏢 塔樓隊 → 玻璃帷幕 │
│ │
│ ⚡ build_mep: │
│ 🏠 小宅隊 → 家用級水電 │
│ 🏢 塔樓隊 → 商用級水電 │
│ │
└──────────────────────────────────────────────────────────┘
╔═══════════════╦═══════════════════════╦═══════════════════════════╗
║ 🎭 角色 ║ ✅ 職責 ║ ❌ 不負責 ║
╠═══════════════╬═══════════════════════╬═══════════════════════════╣
║ 👨💼 Director ║ 🎯 控制建造順序 ║ 🔧 具體施工細節 ║
║ (總監) ║ 📊 流程監控 ║ 🧱 材料選擇 ║
║ ║ 📦 最終交付 ║ ⚒️ 工法實作 ║
╠═══════════════╬═══════════════════════╬═══════════════════════════╣
║ 👷♂️ Builder ║ 🔨 具體施工實作 ║ 📋 決定建造順序 ║
║ (施工隊) ║ 🧱 材料與工法 ║ 🎯 流程控制 ║
║ ║ 🔧 產品組裝 ║ ✅ 品質驗收 ║
╠═══════════════╬═══════════════════════╬═══════════════════════════╣
║ 🏛️ Building ║ 📦 封裝成品資料 ║ 🏗️ 建造過程管理 ║
║ (建築成品) ║ 🔍 提供查詢介面 ║ ✏️ 後續修改 ║
║ ║ 🔒 保證不可變性 ║ 📊 施工狀態追蹤 ║
╚═══════════════╩═══════════════════════╩═══════════════════════════╝
🎨 Builder 模式的美學
┌────────────────────────────────────────────────────────────┐
│ │
│ 「同一套舞譜,不同的舞者」 │
│ │
│ 🎼 Director = 指揮家,掌握節拍與順序 │
│ 💃 Builder = 舞者,詮釋每個動作的風格 │
│ 🎭 Product = 完美演出,一次定型永不更改 │
│ │
│ ✨ 核心價值:流程標準化 + 實作個性化 │
│ │
└────────────────────────────────────────────────────────────┘
🔍 Builder 模式健康檢查
╔══════════════════════════════════════════════════════════════╗
║ ✅ 良好實踐 ║
╠══════════════════════════════════════════════════════════════╣
║ 🎯 Director 只呼叫介面方法,不關心具體實作 ║
║ 🔒 get_product() 回傳不可變物件 ║
║ 🔄 get_product() 呼叫後自動重置 Builder ║
║ 📋 Builder 介面完整定義所有建造步驟 ║
║ 🧪 同一套測試腳本能驗證所有 Builder 變體 ║
╚══════════════════════════════════════════════════════════════╝
╔══════════════════════════════════════════════════════════════╗
║ 🚨 危險信號 ║
╠══════════════════════════════════════════════════════════════╣
║ ⚠️ Director 出現 isinstance() 檢查具體 Builder ║
║ 💥 產品物件交付後還能被外部修改 ║
║ 🔴 連續呼叫 get_product() 得到不一致結果 ║
║ 🌪️ Builder 步驟之間有隱藏的狀態依賴 ║
║ 📈 Builder 類別數量爆炸式增長 ║
╚══════════════════════════════════════════════════════════════╝