iT邦幫忙

2025 iThome 鐵人賽

DAY 13
0
Software Development

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

Day 15:Strategy:同一路口,不同疏運——「政策像開關一樣切換」

  • 分享至 

  • xImage
  •  

Codetopia 創城記 (15)|Strategy:同一路口,不同疏運——「政策像開關一樣切換」

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

Codetopia 的週末通常是悠閒的,但這個週末例外。備受矚目的「海港音樂祭」偏偏撞上了氣象局的豪雨特報。城市交通系統瞬間陷入兩難:白天陽光普照,數萬人潮需要「疏運優先」的交通策略,將他們快速送達會場;可一旦入夜,雷雨胞準時光臨,策略就必須立刻切換為「安全優先」,引導市民分散到臨時避難所,並緊急加密公車班次。

政策分析師 Quinn 的手機響起,螢幕上閃爍著市長室的來電,語氣十萬火急:「Quinn!兩小時內,我要一個方案,一個能『不更新市民 App』就能即時切換交通策略的方案!」

Quinn 眼前浮現出交通控制中心的程式碼——那是一片由無數 if-else 交織成的熱帶雨林。晴天算法、雨天算法、活動日算法、尖峰算法……它們像藤蔓一樣死死纏繞,誰敢動其中一條,整片森林都會崩塌。

(看來,今晚又是一個屬於咖啡因和英雄的夜晚。)

2. 術語卡

🧭 Strategy(策略模式):允許在執行期間,為同一個上下文(Context)替換不同的演算法或政策,同時保持對外介面不變。

🧭 Push/Observer → Strategy:這是一對黃金拍檔。記得我們 Day 14 的「城市廣播台」(Observer)嗎?它發布一則 policy.changed 事件,各個服務(例如交通中心)收到後,就能立刻拉取新的策略(Strategy)來應用。

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

讓我們把鏡頭倒回災難發生前,交通資料工程師 Owen 向 Quinn 展示了現有的 TrafficController.dispatch() 函式,那是一首長達三百行的史詩級悲劇:

# 千萬別學的範例
def dispatch(request):
    if is_festival() and not is_raining():
        return shortest_path_algorithm(request)
    elif is_festival() and is_raining() and has_thunderstorm_warning():
        return evacuate_then_bus_boost_algorithm(request)
    elif is_weekday() and is_peak_hour() and is_light_rain():
        return weight_by_realtime_speed_algorithm(request)
    # ...底下還有 294 行類似的排列組合,看到這裡我的眼睛已經開始流淚...

這味道實在太沖了!👃

壞味道

  • 條件叢林:每增加一個天氣變數或活動類型,複雜度就指數級爆炸,根本沒人敢改。

  • 無法灰度測試:沒有緩衝,一上線就是全城大風吹,出錯了就是全面癱瘓。

  • 客戶端耦合:市民的 App 端居然要知道現在後台是用哪一套演算法在跑,這太荒謬了!

  • 維運惡夢:臨時為了活動改了邏輯,活動結束後忘了改回來怎麼辦?只能燒香拜佛祈禱了。

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

一句話概括:我們不把各種演算法寫死,而是把它們各自打包成獨立的「政策包」(策略物件)。交通疏運中心(Context)手上隨時可以持有一個政策包,並且能隨時替換。至於該換成哪個包?這件事交給外部的「政策開關」或我們昨天提到的「城市廣播」事件來決定就好。

何時用 (When to Use)

  • ✅ 當你有很多種演算法,而且它們需要在執行期間動態切換時。

  • ✅ 當你需要對不同演算法進行 A/B 測試或灰度發布(Canary Release)時。

  • ✅ 當你想把「選擇哪個策略」的邏輯與「策略內部如何實作」的邏輯徹底分離時。

何時不要用 (When NOT to Use)

  • ⛔ 如果你的流程是固定的,只是某些步驟的實作不同 ➡️ 那更適合用 Template Method(樣板方法模式)

  • ⛔ 如果你需要的是協調多個不同組件的複雜互動 ➡️ 那應該是 Mediator(中介者模式) 的主場。

  • ⛔ 如果你的策略只有兩三種,且非常單純 ➡️ 也許一個簡單的函式字典(Dictionary/Map)就夠了,別殺雞用牛刀。

  • 常見混淆:State vs. Strategy ➡️ 如果行為的改變是由物件內部狀態自動觸發的(例如紅綠燈從紅燈變綠燈),那是 State 模式;如果行為的改變是由外部客戶端或設定來決定的,那才是 Strategy 模式

5. 導播切景 (三層並置圖)

導播,麻煩鏡頭拉一下!讓我們用三個不同的縮放層級,看看這個「政策切換」在 Codetopia 是如何運作的。

  • ① 微觀 (GoF):聚焦在物件之間的合作結構。

  • ② 中觀 (事件驅動):觀察訊息如何在系統中流動。

  • ③ 宏觀 (多代理系統):看更上層的角色如何分工與協商。

視角 觀念/模式 在 Codetopia 的說法
微觀 (GoF) Strategy (e.g., RoutingStrategy) 「政策包」可隨時插拔;疏運中心 (Context) 負責執行
中觀 (EIP/EDA) Topic policy.changed + Pull 配置 Day 14 的廣播台發布通知,各服務主動向「策略庫」拉取新策略
宏觀 (MAS) PolicyAgent 決策、DF (黃頁) 公告可用能力 政策代理 (PolicyAgent) 透過黃頁服務 (DF/PolicyRegistry) 發布可用的策略清單

Mermaid|類圖 (微觀結構)

https://ithelp.ithome.com.tw/upload/images/20250929/20178500WzT1ssTvnd.png

Mermaid|時序圖 (中/宏觀資訊流)

https://ithelp.ithome.com.tw/upload/images/20250929/20178500wxn5tPyeB0.png

6. 最小實作 (Pseudo Code)

這就是 Quinn 畫給 Owen 的設計藍圖,清爽、乾淨,而且能睡個好覺。它引入了 Plan 作為資料契約,並用 STRATEGY_FACTORY 徹底消滅了選擇策略的 if-else

from dataclasses import dataclass
from typing import Any, Type

# 0. 定義輸出的資料契約 (Data Contract)
@dataclass(frozen=True)
class Plan:
    steps: list[str]
    safety_score: float  # 0.0 (低安全) ~ 1.0 (高安全)
    meta: dict[str, Any]

# 1. 定義策略介面
class RoutingStrategy:
    def route(self, request) -> Plan:
        raise NotImplementedError

# 2. 實作各種具體的策略
class ShortestPath(RoutingStrategy):
    def route(self, request) -> Plan:
        return Plan(steps=["Go straight", "Turn right"], safety_score=0.60, meta={})

class TrafficAware(RoutingStrategy):
    def route(self, request) -> Plan:
        return Plan(steps=["Take highway", "Exit 3"], safety_score=0.75, meta={})

class RainSafe(RoutingStrategy):
    def route(self, request) -> Plan:
        return Plan(steps=["Follow evacuation route"], safety_score=0.95, meta={})

# 3. 建立執行上下文 (Context)
class DispatchContext:
    def __init__(self, strategy: RoutingStrategy):
        self._strategy = strategy

    def set_strategy(self, strategy: RoutingStrategy):
        # 在高併發環境中,這裡需要以鎖 (lock) 或 RCU (Read-Copy-Update) 等思路實作原子交換 (atomic swap) 來保證執行緒安全
        print(f"策略已切換為: {strategy.__class__.__name__}")
        self._strategy = strategy

    def handle(self, request) -> Plan:
        return self._strategy.route(request)

# 4. 使用工廠模式來取代 if-else
# (在真實系統中,這個註冊表可以動態載入,例如從 PolicyRegistry 或設定檔中讀取,以支援策略的熱插拔)
STRATEGY_FACTORY: dict[str, Type[RoutingStrategy]] = {
    "ShortestPath": ShortestPath,
    "TrafficAware": TrafficAware,
    "RainSafe": RainSafe,
}

# --- 政策切換的魔法時刻 (由 Day 14 的廣播台觸發) ---
def on_policy_changed(event):
    strategy_name = event.get("routing")
    klass = STRATEGY_FACTORY.get(strategy_name)
    if not klass:
        print(f"錯誤:找不到名為 {strategy_name} 的策略")
        return
    # 從工廠取得策略實例並設定
    ctx.set_strategy(klass())

# 模擬使用
ctx = DispatchContext(ShortestPath())
print(ctx.handle("Request from A to B"))

# 模擬收到廣播事件
on_policy_changed({"routing": "RainSafe"})
print(ctx.handle("Request from A to B"))

看到了嗎?DispatchContext 從頭到尾都不知道自己現在用的是哪套演算法,它只管執行。這就是分離的藝術!

7. 反模式紅旗 🚩

當心!即使是好的模式也可能被誤用。看到以下情況請立刻鳴笛警告:

  • 策略洩漏 (Leaky Strategy):Context 不小心對外暴露了太多具體策略的細節,導致呼叫端又開始寫 if (context.currentStrategy is RainSafe) 這種判斷,那一切又回到了原點。

  • 狀態肥大化 (Bloated State):把整個城市的即時狀態(像是全域變數)塞進策略物件裡。這會導致策略切換時,舊的狀態可能殘留下來,造成幽靈般的 bug。

  • 策略過度細化 (Strategy Over-Granularity):為了一丁點的差異就建立一個新策略,最後變成「一城百策」,難以管理。缺少版本號或下線機制更是災難。

  • 選擇邏輯寫死 (Hardcoded Selection Logic):用來選擇策略的地方,又變成了一大坨 if-else。選擇策略的邏輯本身應該由設定檔或外部事件來驅動,而不是寫死在程式碼裡。(我們剛才用 Factory 解決了這個問題!)

8. 城市望遠鏡 (進階視野)

如果我們把視角拉得更高,策略模式可以演化成更強大的架構:

  • Actor 模型:我們可以有一個 PolicyAgent (政策代理人),它專門監控城市的各項指標(降雨量、人潮密度、交通延遲),在一個 Supervisor (監督者) 的看管下,平滑地切換交通中心的策略,並向系統回報切換結果。

  • 服務治理:在我們的 DF(黃頁服務,也就是 PolicyRegistry)中,可以註冊所有可用的策略及其服務等級目標 (SLO)。廣播台只需要廣播策略的名稱和版本號,各個服務再自己去黃頁拉取對應的策略實作。

9. ✅ 回到現場 (驗收時刻)

兩小時後,Quinn 和 Owen 帶著新架構向市長室回報。讓我們用當時的場景來驗收:

  • Given: 音樂祭現場人流 6 萬人,晚間 20:00 準時降下雷陣雨。

  • When: Quinn 透過控制台發布了一條 policy.changed 事件:{"routing":"RainSafe", "fare":"EventOffPeak"}

  • Then:

    1. 全市的交通疏導在 15 分鐘內切換完畢,95% 的市民等待疏散時間小於 6 分鐘。

    2. 市民手機上的 App 和路邊的智慧看板無需任何更新,就自動顯示出前往避難點的路線和加密後的公車班次。

    3. 監控後台顯示,策略切換過程中,系統沒有出現 5xx 錯誤峰值,整體錯誤率低於 0.3%。

    4. 相較於活動前一週的交通基線,P95 等待時間改善 40%,尖峰時段錯誤率下降 75%。

拍案! 這就是優雅架構的力量。

但英雄的工作還沒結束,為了確保城市長治久安,他們還加上了兩道保險:

  • 📈 可觀測性:為每次策略應用加上了關鍵指標,例如 strategy_apply_latency_ms(策略應用延遲)、strategy_adoption_ratio{strategy,version}(各版本策略採納率)、route_error_rate(路線規劃錯誤率)。

  • 🕹️ 治理與風控:在策略庫 (PolicyRegistry) 中加入了停用開關 (disable flag) 與一鍵回退 (rollback) 到上一穩定版本的功能,確保任何失控的策略 (如 RainSafe@2025.09.29) 都能被即時關閉。

  • 同時,版本號採 YYYY.MM.DD 命名(例如 RainSafe@2025.09.29),方便對齊日內熱修、審計與回滾。

10. 測試指北

這麼漂亮的架構,該怎麼測試呢?

  • 契約測試 (Contract Testing):確保所有 RoutingStrategy 的實作,對於同一個請求,都能回傳語義上等價的 Plan 物件(例如,該有的欄位都有)。

  • 切換測試 (Switching Test):寫一個測試,對 DispatchContext 反覆注入三種不同的策略,然後連續發出 100 次請求,驗證每次的回應都符合當前的策略,沒有任何狀態殘留。

  • 實驗護欄 (Guardrail Test):透過模擬的「城市廣播台」注入假的政策變更事件,驗證線上多個服務實例都能在 1 秒內完成策略的同步更新。

  • 灰度一致性測試 (Grayscale Consistency Test):驗證對於相同的用戶或路段 ID(經過 hash 運算後),在灰度發布期間應始終被分配到同一個策略(新或舊),避免搖擺。

  • 原子切換壓力測試 (Atomic Switch Stress Test):在策略切換的瞬間,模擬大量併發請求(例如每秒 10000 次),確保系統不會回傳混合或不完整的計畫。

    • 屬性導向測試 (Property-based/Fuzz):隨機產生多種天氣/人流組合,驗證任何輸入都能產生結構合法Plan(欄位齊備、safety_score ∈ [0,1]),並確保切換前後不會產生非預期例外。

11. 鄉民出題 (動手時間)

  1. 實作挑戰:請為 DispatchContext 增加一個灰度發布功能。例如,讓 10% 的流量走新的 TrafficAware 策略,90% 的流量走舊的 ShortestPath 策略,並記錄兩者的效能差異。你會如何修改 handle 方法?(提示:灰度分流請務必以一個穩定鍵 (如 userId 或路段 ID) 做 hash 運算,以確保同一主體在灰度發布期間能固定命中同一套策略,避免體驗不一致。)

  2. 情境二選一:假設臨時暴雨來襲,但只集中在城市西區。身為總設計師的你,會選擇:

    • A: 全城統一採用 RainSafe 策略(優點:簡單、一致)。

    • B: 根據地圖格網,只在西區切換為 RainSafe 策略(優點:精準、影響小,但實作複雜)。

請在留言區留下你的選擇 (A/B) 和一句話理由!

12. 結語 & 預告

今日摘要:策略可插拔,事件驅動切換;同一入口,不同算法,灰度上線穩如老狗。

明天,Codetopia 會遇到一個棘手的市民陳情案,它需要經過好幾個部門的審核。我們要如何打造一條能動態編排、權責分明的處理鏈呢?

明日預告:Day 16|Chain of Responsibility (責任鏈模式) —— 這個案子誰該接?讓它在處理鏈上跑一跑就知道!


13. 附錄:ASCII 版圖示

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

策略模式類圖 (Strategy Pattern Class Diagram)

 ┌─────────────────────────┐
 │  <<<interface>>>        │
 │    RoutingStrategy      │
 │─────────────────────────│
 │ + route(request): Plan  │
 └─────────────┬───────────┘
               │ implements
      ┌────────┼────────┐
      ▼        ▼        ▼
 ┌──────────┐ ┌──────────┐ ┌──────────┐
 │ShortestP.│ │TrafficAw.│ │ RainSafe │
 │──────────│ │──────────│ │──────────│
 │+route()  │ │+route()  │ │+route()  │
 └──────────┘ └──────────┘ └──────────┘

          ┌─────────────────────────────┐
          │     DispatchContext         │
          │─────────────────────────────│
          │ - currentStrategy: Strategy │
          │─────────────────────────────│
          │ + setStrategy(strategy)     │
          │ + handle(request): Plan     │
          └──────────┬──────────────────┘
                     │ uses
                     ▼
          ┌─────────────────────────────┐
          │    RoutingStrategy          │
          │    (composition)            │
          └─────────────────────────────┘

事件驅動策略切換流程 (Event-Driven Strategy Switch Flow)

┌─────────┐    ┌─────────────┐    ┌──────────────┐    ┌─────────────┐
│  Quinn  │    │ Broadcast   │    │ PolicyReg    │    │ Dispatch    │
│政策分析師│    │   Center    │    │    策略庫    │    │  Context    │
│         │    │   廣播台    │    │              │    │  疏運中心   │
└────┬────┘    └──────┬──────┘    └──────┬───────┘    └──────┬──────┘
     │                │                  │                   │
     │ 1. publish     │                  │                   │
     │ policy.changed │                  │                   │
     ├───────────────►│                  │                   │
     │                │ 2. notify()      │                   │
     │                ├─────────────────────────────────────►│
     │                │                  │ 3. fetch strategy │
     │                │                  │◄──────────────────┤
     │                │                  │                   │
     │                │                  │ 4. return factory │
     │                │                  ├──────────────────►│
     │                │                  │                   │
     │                │                  │      5. setStrategy()
     │                │                  │                   │
┌─────────┐                              │                   │
│Citizen  │                              │                   │
│  App    │           6. requestRoute(A→B)                   │
└────┬────┘           ◄─────────────────────────────────────┤
     │                                    │                   │
     │                7. plan via RainSafe                   │
     │                ◄─────────────────────────────────────┤
     │                                    │                   │

策略工廠註冊表 (Strategy Factory Registry)

    ┌───────────────────────────────────────────────────────┐
    │              STRATEGY_FACTORY                         │
    │═══════════════════════════════════════════════════════│
    │  Key           │  Implementation Class                │
    │──────────────────────────────────────────────────────│
    │ "ShortestPath" │  ShortestPath     ──┬──► 🏃‍♂️ 最短路徑  │
    │ "TrafficAware" │  TrafficAware     ──┼──► 🚦 即時路況  │
    │ "RainSafe"     │  RainSafe         ──┼──► ☔ 雨天安全  │
    │                │                     │                │
    │ (動態載入...)   │  CustomStrategy   ──┘──► 🔧 自定義策略 │
    └───────────────────────────────────────────────────────┘
                         ▲
                         │ get(strategyName)
    ┌────────────────────┴─────────────────────┐
    │         on_policy_changed()              │
    │  ┌─────────────────────────────────────┐ │
    │  │  1. 解析事件參數                      │ │
    │  │  2. 查找工廠註冊表                    │ │
    │  │  3. 創建策略實例                      │ │
    │  │  4. 原子切換 Context 策略             │ │
    │  └─────────────────────────────────────┘ │
    └───────────────────────────────────────────┘

交通策略對比表 (Traffic Strategy Comparison)

策略類型     │ 優先考量        │ 適用場景          │ 安全分數
─────────────┼─────────────────┼──────────────────┼─────────
ShortestPath │ 距離最短        │ 日常通勤          │  ★★★☆☆
             │ 🎯 效率導向     │ 晴朗天氣          │  (0.60)
─────────────┼─────────────────┼──────────────────┼─────────
TrafficAware │ 即時路況        │ 尖峰時段          │  ★★★★☆
             │ 🚦 智能迴避     │ 活動期間          │  (0.75)
─────────────┼─────────────────┼──────────────────┼─────────
RainSafe     │ 安全至上        │ 惡劣天氣          │  ★★★★★
             │ ☔ 避險優先     │ 緊急疏散          │  (0.95)

策略切換生命週期 (Strategy Switch Lifecycle)

初始化階段         執行階段           切換階段

┌──────────┐    ┌──────────┐    ┌──────────┐
│ 策略載入  │───►│ 處理請求  │───►│ 收到切換  │
│          │    │          │    │ 事件     │
│[Factory] │    │[Context] │    │[Event]   │
└──────────┘    └─────┬────┘    └─────┬────┘
                      │               │
                      ▼               ▼
               ┌──────────┐    ┌──────────┐
               │ 執行策略  │    │ 原子切換  │
               │ route()  │    │ 新策略   │
               └─────┬────┘    └─────┬────┘
                     │               │
                     ▼               ▼
               ┌──────────┐    ┌──────────┐
               │ 回傳計畫  │    │ 繼續服務  │
               │ Plan{}   │    │ (新策略)  │
               └──────────┘    └──────────┘
                     ▲               │
                     │               │
                     └───────────────┘
                      (循環處理)

灰度發布流量分配 (Canary Release Traffic Distribution)

用戶請求流量分配 (基於 Hash 分片)

   100% 流量    ┌─────────────────────┐
      │         │   Hash Router       │
      ▼         │   (基於 userId)     │
┌──────────┐    └─────────┬───────────┘
│ 全量用戶  │              │
│ 請求     │              ▼
└────┬─────┘    ┌─────────────────────┐
     │          │   流量分流決策       │
     │          │   hash % 100        │
     │          └─────────┬───────────┘
     │                    │
     ▼                    ▼
┌──────────┐         ┌─────────┐   ┌─────────┐
│ 90% 流量 │         │ 10%流量 │   │ 策略A   │
│ 走舊策略 │   OR    │ 走新策略│   │ 穩定版  │
│ 📊 監控  │         │ 📈 實驗 │   │ 策略B   │
└──────────┘         └─────────┘   │ 測試版  │
                                   └─────────┘

    ┌─────────────────────────────────────────┐
    │          效能比較與回滾機制              │
    │  ┌─────────────┐    ┌─────────────┐    │
    │  │ 策略A指標   │    │ 策略B指標   │    │
    │  │ 延遲: 50ms  │    │ 延遲: 45ms  │    │
    │  │ 錯誤: 0.1%  │    │ 錯誤: 0.3%  │    │
    │  │ 滿意度: 95% │    │ 滿意度: 97% │    │
    │  └─────────────┘    └─────────────┘    │
    └─────────────────────────────────────────┘


上一篇
Day 14:Observer:城市廣播、訂閱更新——一呼百應的事件之城
下一篇
Day 16:Chain of Responsibility(責任鏈模式):棘手陳情案的逐級節點
系列文
Codetopia 新手日記:設計模式與原則的 30 天學習之旅17
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言