iT邦幫忙

2025 iThome 鐵人賽

DAY 13
0
Software Development

Codetopia 新手日記:設計模式與原則的 30 天學習之旅系列 第 22

Day 22:Visitor (訪客模式):十分鐘產出三份報表,不動原物件!

  • 分享至 

  • xImage
  •  

Codetopia 創城記 (22)|Visitor (訪客模式):十分鐘產出三份報表,不動原物件!

1) 今日熱點 (故事開場 & 痛點) ⚡️

Codetopia 海港音樂祭的最後一個音符剛落下,熱情的安可聲還在空中迴盪,但市府的後台已經炸開了鍋。市長的緊急指令透過加密頻道傳來,語氣不容置疑:「十分鐘後,我要在辦公桌上看到三份即時報表!」

哪三份?

① 安全稽核:所有舞台、路障、臨時電纜是否符合安全規範?

② 成本分攤:那些臨時設施的租金和工程人員的加班費,怎麼精確分攤到各個表演區域?

③ 個資遮罩:活動期間登記的志工與 VIP 清單,必須依法規完成去識別化處理,才能歸檔。

Gavin|資產維護工程師,此刻的表情比斷線的伺服器還要凝重。他手上的「城市資產樹」——那個由 舞台 (Stage)區域 (Zone)路障 (RoadBlock)電纜 (Cable) 等元素構成的穩定結構——已經安穩運行好幾個月了。要為這幾十種元素,同時加上三種完全不同的行為邏輯?這意味著什麼?

(旁白內心戲:「這意味著要修改幾十個 class 檔案,然後祈禱CI/CD大神保佑不要改壞任何東西......在十分鐘內?」)

Gavin 的手指已經懸在鍵盤上,準備開始一場史詩級的複製貼上大戰。

就在這時,一個冷靜的聲音從他身後傳來。「Gavin,停下來。別碰那些元素。」

是 Vera|稅務稽核官,她手上拿著一杯咖啡,神情自若。「我們不需要動手術,只需要派幾個『訪客』進去逛一圈就行了。」

「訪客?」Gavin 一臉困惑。

「對,」Vera 推了推眼鏡,「把你的新行為做成『訪客』,讓它們去拜訪你的資產樹。稽核、算錢、打馬賽克......各司其職,互不干擾。資產樹本身,一根毛都不用動。」

砰! 這簡直是黑暗中的一道閃電。在固定結構上,外掛源源不絕的新操作,這不正是應對需求變化的終極戰術嗎?

2) 術語卡 🧭

  • Visitor (訪客/外掛行為):當一組物件結構相對固定時,可以將作用於這些物件的「新操作」封裝成一個獨立的 Visitor 物件。透過 element.accept(visitor) 這一巧妙的雙重派發 (double dispatch) 機制,我們就能在不修改元素類別的前提下,為它們新增無限的行為。核心精神是:對新增操作開放,對修改元素封閉。(旁批:但請記住,這份契約的代價是,當你需要新增元素型別時,所有現存的 Visitor 族群可能都需要同步擴充!)

  • Element (元素):被訪問的物件,是固定結構中的節點。在今天的故事裡,就是 StageZoneRoadBlockCable 等資產。

  • Visitor (訪客):封裝了特定行為的物件,例如 SafetyCheckVisitorCostSplitVisitorPiiRedactionVisitor

  • 常見親戚:Visitor 模式常常與 Composite (組合模式) 聯手,用來處理樹狀結構;與 Iterator (迭代器) 合作,統一遍歷路徑;與 Memento (備忘錄) 搭配,為稽核的狀態留下快照。

3) 笑中帶淚 (反例/壞味道) 😂

讓我們倒帶一下,如果當時 Vera 沒有及時出現,Gavin 的「快狠準」方案會把 Codetopia 帶向何方?

Gavin 心一橫,決定在每個元素類別(Stage, Zone, RoadBlock, Cable...)上,直接新增三個 public 方法:auditSafety()calculateCost()redactPii()。看起來很直觀,對吧?

災難就此開始:

  • 違反開放封閉原則:市長隔天又要一份「碳排估算」報表。Gavin 只好含淚加班,再次修改所有的元素類別,新增 estimateCarbonFootprint() 方法。城市資產每多一種,這份修改清單就變得更長。

  • 行為邏輯四散:單單一個「安全稽核」的邏輯,現在像天女散花一樣,分散在十幾個不同的 class 檔案裡。想修改一條稽核規則?祝你在程式碼的汪洋大海中好運。這根本是測試工程師的地獄。

  • 分支地獄:很快地,程式碼裡到處都是 if (element instanceof Stage) { ... } else if (element instanceof RoadBlock) { ... } 的醜陋分支。而這些判斷,本該是由 Visitor 的雙重派發機制優雅地消滅掉的!

驗收時刻:當「碳排估算」的需求真的下來時,Gavin 的分支已經改不動了。整個資產模組的部署被死死卡住,因為每次小修改都可能引發雪崩式的錯誤。市長的報表?大概要等到下個世紀了。

4) 王牌出手 (核心觀念/何時用/不適用) 👑

Vera 的提案,正是設計模式中的 Visitor (訪問者模式)。它的核心思想,一句話就能概括:

當資料結構 (元素型別) 趨於穩定,但需要對其定義的新操作 (行為) 卻層出不窮時,就用 Visitor。

✅ 何時用 (When to Use)

  • 對固定結構頻繁新增操作:當你有一個穩定的物件結構(例如:城市資產樹、抽象語法樹 AST、XML/JSON 文件樹),且需要頻繁地在其上執行各種報表生成、驗證、轉換等操作時。

  • 需要將相關行為收斂一處:想把「成本計算」的所有邏輯都放在 CostVisitor 這一個檔案裡,而不是散落在各個 Asset 子類中。這讓邏輯的維護、測試和理解都變得極其容易。

  • 確保元素唯讀與操作併行:一個優良的 Visitor 實作,應只讀取元素狀態,而不修改它。這種「無副作用」的特性,使得我們可以在同一個不可變的資料快照上,安全地併行執行多個不同的訪客,極大地提升效率。(旁批:需要輸出的副作用,如寫檔或呼叫 API,應交給後置的 Exporter 或 Command 物件處理;訪客間若需共享資料,也應透過唯讀的 Context 物件注入,絕不共享可變物件!)

⛔️ 何時不要用 (When NOT to Use)

  • 元素型別經常變動:如果你的資產樹三天兩頭就要新增一種元素(例如今天加 無人機燈光秀 (DroneLight)),那 Visitor 模式會讓你很痛苦。因為每新增一個 Element 子類,你可能就必須去修改所有 Visitor 的介面,為新元素新增一個 visit...() 方法。

  • 只是簡單的策略切換:如果你的問題核心只是在同一個操作中切換不同的演算法(例如計算費用時,分「平日價」和「假日價」),那用更輕巧的 Strategy (策略模式) 就足夠了。

  • 問題核心是多方物件的複雜互動:如果你的場景重點在於協調多個不同物件之間的複雜通訊與流程,避免網狀依賴,那應該交給 Mediator (中介者模式) 來處理。

5) 導播切景 (表格+兩張 Mermaid) 🎬

導播,鏡頭拉一下!讓我們用三個不同的縮放層級,看看 Visitor 模式在 Codetopia 的架構中是如何運作的。

(旁白提示:單趟遍歷的時間複雜度為 O(N×V),其中 N 是節點數,V 是訪客數;空間複雜度為 O(H),H 是樹的高度。)

視角 觀念/模式 在城市的說法
微觀 (GoF) Visitor Pattern (雙重派發) 稽核員 Vera (Visitor) 拜訪各項資產 (Element)
中觀 (EIP/EDA) Pipeline & Filters / Itinerary-based Routing 一個稽核任務 (Message) 帶著行程單,依序通過各個訪客站點
宏觀 (MAS) Agent with specific capability 稽核總代理 (Vera) 動態指派一組專職訪客代理去執行任務

微觀 (GoF)|UML 類別圖 (Visitor 的雙重派發結構)

https://ithelp.ithome.com.tw/upload/images/20251006/20178500PO1tOcbQ7e.png

中觀 (EIP/EDA)|一次運行多個訪客的稽核流程

https://ithelp.ithome.com.tw/upload/images/20251006/20178500tNYn4T2rvU.png

6) 最小實作 (Python 範例) 💻

Vera 迅速在白板上寫下一段 Python 程式碼,展示了她的「訪客」提案如何落地。

(旁白提示:此版本將遍歷責任外移到 run_audit_one_pass 這個 Runner 函式中,元素的 accept() 方法僅專注於雙重派發這一件事。請勿在 accept() 內再遞迴呼叫子節點,以免重複拜訪!)

from abc import ABC, abstractmethod
from dataclasses import dataclass, field
from typing import List, Protocol
from decimal import Decimal, ROUND_HALF_UP

# --- 1) 元素階層(這部分結構是固定的,不再輕易改動) ---
class Visitor(Protocol):
    """定義訪客的介面,每個具體元素都有一個對應的 visit 方法"""
    def visit_stage(self, e: "Stage"): ...
    def visit_zone(self, e: "Zone"): ...
    def visit_roadblock(self, e: "RoadBlock"): ...
    def visit_cable(self, e: "Cable"): ...

class Asset(ABC):
    """所有資產元素的抽象基底,只定義一個 accept 方法"""
    @abstractmethod
    def accept(self, v: Visitor): ...

@dataclass
class Stage(Asset):
    name: str
    zones: List["Zone"] = field(default_factory=list)
    def accept(self, v: Visitor):
        v.visit_stage(self)

@dataclass
class Zone(Asset):
    name: str
    items: List[Asset] = field(default_factory=list)
    volunteers: List[str] = field(default_factory=list) # 假設這裡含有個資
    def accept(self, v: Visitor):
        v.visit_zone(self)

@dataclass
class RoadBlock(Asset):
    id: str
    rented_hours: int
    staff_count: int
    def accept(self, v: Visitor):
        v.visit_roadblock(self)

@dataclass
class Cable(Asset):
    id: str
    meters: int
    amperage: int
    def accept(self, v: Visitor):
        v.visit_cable(self)

# --- 2) 訪客家族:所有的新增行為都收斂在這裡! ---
class BaseVisitor(ABC):
    """提供一個 no-op (無操作) 的基底,讓具體訪客只需覆寫關心的方法"""
    def visit_stage(self, e: Stage): pass
    def visit_zone(self, e: Zone): pass
    def visit_roadblock(self, e: RoadBlock): pass
    def visit_cable(self, e: Cable): pass

class SafetyCheckVisitor(BaseVisitor):
    """安全稽核訪客:只關心路障和電纜"""
    def __init__(self): self.issues = []
    def visit_roadblock(self, e: RoadBlock):
        # 根據工安標準,每個路障至少需配置 2 名人員
        if e.staff_count < 2:
            self.issues.append(f"RoadBlock {e.id}: 人員配置不足 ({e.staff_count} < 2)")
    def visit_cable(self, e: Cable):
        if e.amperage > 32:
            self.issues.append(f"Cable {e.id}: 電流過載 ({e.amperage}A > 32A)")

class CostSplitVisitor(BaseVisitor):
    """成本分攤訪客:眼中只有錢"""
    def __init__(self):
        self.total_cost = Decimal("0")
    def visit_roadblock(self, e: RoadBlock):
        self.total_cost += Decimal("200.0") * e.rented_hours
    def visit_cable(self, e: Cable):
        self.total_cost += Decimal("5.5") * e.meters

class PiiRedactionVisitor(BaseVisitor):
    """個資遮罩訪客:只關心志工名單"""
    def __init__(self): self.redacted_count = 0
    def visit_zone(self, e: Zone):
        # 在真實世界,這裡會執行遮罩邏輯;此處簡化為計數
        self.redacted_count += len(e.volunteers)

class CarbonFootprintVisitor(BaseVisitor):
    """碳排估算訪客:練習題的參考解答"""
    def __init__(self): self.co2_kg = Decimal("0")
    def visit_roadblock(self, e: RoadBlock):  # 運輸&待機
        self.co2_kg += Decimal("2.5") * e.rented_hours
    def visit_cable(self, e: Cable):      # 用電近似
        self.co2_kg += Decimal("0.02") * e.meters

# --- 3) 執行器:單趟遍歷,一次執行多個訪客,效能 UP! ---
def run_audit_one_pass(root: Asset, visitors: List[Visitor]):
    """使用顯式的迭代器(此處用堆疊模擬),單趟走完資產樹"""
    stack = [root]
    while stack:
        current_asset = stack.pop()

        for v in visitors:
            current_asset.accept(v)

        # 這裡的 isinstance 僅用於 Composite 結構的向下展開,
        # 行為的差異則完全交給 Visitor 的雙重派發,避免了 if/elif 分支地獄。
        if isinstance(current_asset, Stage):
            # 為確保輸出可重現,固定使用 LIFO+反向壓棧,得到左到右的拜訪順序
            stack.extend(reversed(current_asset.zones))
        elif isinstance(current_asset, Zone):
            stack.extend(reversed(current_asset.items))

def run_festival_audit(root: Stage):
    # 建立訪客實例
    visitors = [
        SafetyCheckVisitor(),
        CostSplitVisitor(),
        PiiRedactionVisitor(),
        CarbonFootprintVisitor()
    ]

    # 執行單趟稽核
    run_audit_one_pass(root, visitors)

    safety_v, cost_v, pii_v, carbon_v = visitors

    # 彙整結果。邏輯層保留 Decimal,呈現層才做格式化
    cost = cost_v.total_cost.quantize(Decimal("0.01"), rounding=ROUND_HALF_UP)
    co2 = carbon_v.co2_kg.quantize(Decimal("0.1"), rounding=ROUND_HALF_UP)
    return {
        "safety_report": safety_v.issues,
        "cost_report": f"${cost}", # 幣別與稅率口徑由訪客內部策略決定
        "pii_report": f"{pii_v.redacted_count} items redacted",
        "carbon_report": f"{co2} kg CO2e"
    }

# 建立一個模擬的資產樹
if __name__ == "__main__":
    # 建立一個模擬的資產樹(僅示範;測試時不會自動執行)
    main_stage = Stage("主舞台", zones=[
        Zone("搖滾區", items=[
            RoadBlock("RB-01", rented_hours=8, staff_count=3),
            Cable("C-01", meters=100, amperage=30)
        ], volunteers=["Ava", "Bob"]),
        Zone("後台區", items=[
            RoadBlock("RB-02", rented_hours=12, staff_count=1),  # <-- 安全問題
            Cable("C-02", meters=50, amperage=40)               # <-- 安全問題
        ], volunteers=["Charlie"])
    ])
    # 執行稽核!
    reports = run_festival_audit(main_stage)
    print(reports)

7) 鄉民出題 (動手+反模式紅旗) 🚩

🚩 反模式紅旗 (Red Flags)

  • 元素狂長症:當你發現自己新增 Element 子類的頻率遠高於新增 Visitor 時,這是一個強烈的訊號,表示你的抽象可能選錯了,Visitor 模式正在成為你的負擔。

  • 肥胖訪客:一個 Visitor 類別同時負責驗證、計算、寫入檔案、發送通知......這違反了單一職責原則。應該將它們拆分為「純計算的 Visitor」和處理副作用的「Command 物件」或輸出器。

  • 濫用 RTTI:如果在 Visitor 的方法中還看到大量的 isinstance()typeof 檢查來決定行為,那表示你沒有完全理解雙重派發的精髓,模式的優雅之處被破壞了。

  • 跨訪客秘密通訊:如果 Visitor A 需要偷偷讀取 Visitor B 的內部狀態才能工作,這是一個危險的設計。應該透過一個共享的、明確的 ContextReportDTO 物件來傳遞資訊,而不是讓訪客之間產生隱晦的耦合。

🧠 鄉民出題

  1. 動手實作:市長的新需求來了——「碳排估算報表」。請你新增一個 CarbonFootprintVisitor,規則自訂(例如:舞台耗電、路障運輸會產生碳排)。關鍵挑戰是:你不准修改任何 Asset 相關的類別!(提示:解答已包含在範例程式碼中。)

  2. 情境思考:在 3) 笑中帶淚 的故事中,如果可憐的 Gavin 已經把 auditSafety() 等方法直接加到了元素類別上,現在 Vera 要來拯救他。你會建議他如何一步步地將程式碼重構 (Refactor) 到正確的 Visitor 模式?第一步該做什麼?

  3. 小投票:如果現在又有一個需求,是要在稽核過程中「儲存稽核結果到不同的格式 (JSON, XML, CSV)」。你會選擇:

    A. 讓每個 Visitor (Safety, Cost) 自己負責三種格式的輸出?

    B. 再新增三個 JsonOutputVisitor, XmlOutputVisitor, CsvOutputVisitor,讓它們去訪問「報表物件」?

    請在心裡選擇 A 或 B,並思考你的理由。

8) 測試指北 🧪

為我們的稽核流程加上一個簡單的保護網是很有必要的。這裡是一個使用 pytest 的極簡範例,確保我們的安全規則有被正確觸發:

# test_audit.py
# (假設上面的程式碼存在名為 festival_audit 的檔案中)
from festival_audit import run_festival_audit, Stage, Zone, RoadBlock, Cable

def test_safety_rules_for_insufficient_staff():
    """測試:當路障人員不足時,應產生安全警示"""
    # 建立一個有問題的資產樹
    test_stage = Stage("測試舞台", zones=[
        Zone("問題區", items=[
            RoadBlock("RB-99", rented_hours=1, staff_count=1) # 人員不足
        ])
    ])

    reports = run_festival_audit(test_stage)

    # 斷言:安全報告中應包含關於 "RB-99" 的警示
    assert any("RB-99" in issue for issue in reports["safety_report"])

9) 城市望遠鏡 (進階視野) 🔭

今天的 Visitor 模式看似只解決了物件層級的問題,但將視角拉高,它其實是更宏大架構思想的縮影:

  • 與 Memento (備忘錄):在執行任何稽核 Visitor 之前,先對整個資產樹使用 Memento 模式創建一個不可變的快照 (snapshot_id = snapshot_service.freeze(main_stage))。將這個 ID 傳給所有訪客 (v.set_snapshot_id(snapshot_id)),可以確保每次稽核的結果都基於同一個基準點,完全可重現、可追溯,為下一章的故事完美鋪陳。(旁批:快照應以不可變資料結構,如 frozen dataclass 或序列化後的 blob 保存,避免審計期間因共享物件被改動而失真。)

  • Pythonic 訪客:在 Python 的生態系中,除了傳統的 Protocol 介面,我們還有更靈活的選擇。例如,我們可以不定義固定的 Visitor 介面,而是透過命名約定來動態分派。這只需一行程式碼就能實現:getattr(visitor, f'visit_{type(element).__name__.lower()}', lambda e: None)(element)。這種方式在新增元素時,只需為關心的訪客添加對應方法即可,無需驚動整個訪客家族。

  • 升維至多代理系統 (MAS):在更宏觀的視角下,Vera 可以扮演一個 AuditorAgent 的角色。當接到任務時,她不是親自執行,而是去城市的「黃頁服務 (Directory Facilitator)」查詢所有已註冊的、具備稽核能力的 VisitorAgent,然後將任務並行派發給它們,最後再彙整結果。

10) 結語 & 預告

九分鐘後,四份完美格式化的報表準時出現在市長的桌上。Gavin 看著 Vera,眼神充滿了敬佩。今天,他們不僅解決了一場危機,更為 Codetopia 的架構武器庫中,增添了一件強大的兵器。

今日核心:固定結構、外掛行為——Visitor 讓變化不再是惡夢。

稽核報表是搞定了,但如果跑完報表才發現,其中一個步驟的資料來源是錯的,整個流程都想反悔,該怎麼辦?難道只能手動回滾嗎?

別擔心!明天,我們將拜訪 Codetopia 最神秘的機構——「時光局 (Memento)」,學習如何優雅地存檔與還原,讓我們擁有穿梭時空、一鍵回溯的超能力!


附錄:ASCII 版圖示

為了確保在不支援 Mermaid 渲染的環境中也能正常閱讀,以下提供文中圖表的 ASCII 替代版本:

1) Visitor 模式類別圖 (ASCII 版)

┌─────────────────────────────────────────────────────────────────────┐
│                          Visitor Pattern                           │
└─────────────────────────────────────────────────────────────────────┘

        Asset                           Visitor
   ┌──────────────┐                ┌─────────────────┐
   │ <<interface>>│                │  <<interface>>  │
   │              │                │                 │
   │ +accept(v)   │                │ +visit_stage()  │
   └──────┬───────┘                │ +visit_zone()   │
          │                        │ +visit_roadb..()│
          │                        │ +visit_cable()  │
    ┌─────┴─────┐                  └────────┬────────┘
    │           │                           │
┌───▼───┐   ┌───▼───┐                      │
│ Stage │   │ Zone  │              ┌───────┴───────┐
│       │   │       │              │               │
└───┬───┘   └───┬───┘        ┌─────▼──────┐ ┌─────▼──────┐
    │           │            │Safety Check│ │ Cost Split │
    │      ┌────▼─────┐      │  Visitor   │ │  Visitor   │
    │      │RoadBlock │      └────────────┘ └────────────┘
    │      └──────────┘              │               │
    │                         ┌──────▼──────┐ ┌─────▼──────┐
┌───▼───┐                     │PII Redaction│ │   Carbon   │
│ Cable │                     │   Visitor   │ │  Footprint │
│       │                     └─────────────┘ │  Visitor   │
└───────┘                                     └────────────┘

雙重派發流程:
asset.accept(visitor) ──→ visitor.visit_xxx(asset)
     │                           │
     └─── 第一次派發              └─── 第二次派發
         (根據 asset 類型)            (根據 visitor 類型)

2) 稽核流程序列圖 (ASCII 版)

┌─────────────────────────────────────────────────────────────────────┐
│                      稽核流程:單趟遍歷多訊客                          │
└─────────────────────────────────────────────────────────────────────┘

AuditRunner    AssetIterator    SafetyCheck    CostSplit    PiiRedaction
     │               │              │            │             │
     │ 遍歷所有資產      │              │            │             │
     ├──────────────→ │              │            │             │
     │               │              │            │             │
     │ ╔═══════════════════════════════════════════════════════════╗
     │ ║                  For each asset                          ║
     │ ╚═══════════════════════════════════════════════════════════╝
     │               │              │            │             │
     │ ←─────────────┤ asset        │            │             │
     │               │              │            │             │
     │ accept(SafetyCheck)          │            │             │
     ├──────────────────────────────→            │             │
     │               │              │            │             │
     │ accept(CostSplit)            │            │             │
     ├─────────────────────────────────────────→ │             │
     │               │              │            │             │
     │ accept(PiiRedaction)         │            │             │
     ├────────────────────────────────────────────────────────→ │
     │               │              │            │             │
     │ ╔═══════════════════════════════════════════════════════════╗
     │ ║                    Loop End                              ║
     │ ╚═══════════════════════════════════════════════════════════╝
     │               │              │            │             │
     │ 彙整三份報表      │              │            │             │
     ├──────────→     │              │            │             │
     │               │              │            │             │

優勢:
✓ 單次遍歷 O(N) - 高效能
✓ 訊客互不干擾 - 並行友善
✓ 新增行為無需改動原結構

3) 資產樹結構範例 (ASCII 版)

┌─────────────────────────────────────────────────────────────────────┐
│                        Codetopia 資產樹                            │
└─────────────────────────────────────────────────────────────────────┘

                    🎪 主舞台 (Stage)
                         │
         ┌───────────────┴───────────────┐
         │                               │
     🎵 搖滾區 (Zone)                🎭 後台區 (Zone)
         │                               │
    ┌────┴────┐                     ┌────┴────┐
    │         │                     │         │
🚧 RoadBlock  📡 Cable          🚧 RoadBlock  📡 Cable
   RB-01      C-01               RB-2       C-02
   8hrs,3人   100m,30A           12hrs,1人⚠️ 50m,40A⚠️

志工名單:                          志工名單:
• Ava, Bob                         • Charlie

稽核結果展示:
┌─────────────────────┬─────────────────────────────────────────┐
│      稽核項目        │                結果                      │
├─────────────────────┼─────────────────────────────────────────┤
│ 🔒 安全稽核         │ • RB-02: 人員配置不足 (1 < 2)            │
│                     │ • C-02: 電流過載 (40A > 32A)           │
├─────────────────────┼─────────────────────────────────────────┤
│ 💰 成本分攤         │ $1,875.00                              │
├─────────────────────┼─────────────────────────────────────────┤
│ 🔒 個資遮罩         │ 3 items redacted                       │
├─────────────────────┼─────────────────────────────────────────┤
│ 🌱 碳排估算         │ 33.0 kg CO2e                           │
└─────────────────────┴─────────────────────────────────────────┘

4) 反模式警示圖 (ASCII 版)

┌─────────────────────────────────────────────────────────────────────┐
│                           🚩 反模式紅旗                             │
└─────────────────────────────────────────────────────────────────────┘

❌ 錯誤做法:在每個 Asset 類別直接加方法

  Stage                 Zone                RoadBlock
┌─────────────┐      ┌─────────────┐      ┌─────────────┐
│+auditSafety │      │+auditSafety │      │+auditSafety │
│+calcCost    │  ──→ │+calcCost    │  ──→ │+calcCost    │
│+redactPii   │      │+redactPii   │      │+redactPii   │
│+carbonfPrint│      │+carbonfPrint│      │+carbonfPrint│
└─────────────┘      └─────────────┘      └─────────────┘
      │                     │                     │
      ▼                     ▼                     ▼
   邏輯分散              測試困難               違反 OCP

✅ 正確做法:Visitor 模式集中行為

         Asset樹 (不變)              ←→         Visitor群 (可擴充)
    ┌─────────────────┐                    ┌─────────────────┐
    │ 🎪 Stage       │                    │ SafetyCheck     │
    │ ├── 🎵 Zone    │ ──── accept() ───→ │ CostSplit       │
    │ │   ├── 🚧 RB  │                    │ PiiRedaction    │
    │ │   └── 📡 C   │                    │ CarbonFootprint │
    └─────────────────┘                    └─────────────────┘
          固定結構                              彈性行為

好處:
✓ 新增行為不動原結構      ✓ 邏輯集中易維護
✓ 單一職責原則           ✓ 開放封閉原則
✓ 並行執行安全           ✓ 測試覆蓋容易


上一篇
Day 21:Template Method(骨架+鉤子):一鍵啟動雨備 SOP,不再人眼對拍!
下一篇
Day 23:Memento(時光局):尖峰時段的一鍵「回到剛剛」
系列文
Codetopia 新手日記:設計模式與原則的 30 天學習之旅23
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言