Codetopia 的週末通常是悠閒的,但這個週末例外。備受矚目的「海港音樂祭」偏偏撞上了氣象局的豪雨特報。城市交通系統瞬間陷入兩難:白天陽光普照,數萬人潮需要「疏運優先」的交通策略,將他們快速送達會場;可一旦入夜,雷雨胞準時光臨,策略就必須立刻切換為「安全優先」,引導市民分散到臨時避難所,並緊急加密公車班次。
政策分析師 Quinn 的手機響起,螢幕上閃爍著市長室的來電,語氣十萬火急:「Quinn!兩小時內,我要一個方案,一個能『不更新市民 App』就能即時切換交通策略的方案!」
Quinn 眼前浮現出交通控制中心的程式碼——那是一片由無數 if-else
交織成的熱帶雨林。晴天算法、雨天算法、活動日算法、尖峰算法……它們像藤蔓一樣死死纏繞,誰敢動其中一條,整片森林都會崩塌。
(看來,今晚又是一個屬於咖啡因和英雄的夜晚。)
🧭 Strategy(策略模式):允許在執行期間,為同一個上下文(Context)替換不同的演算法或政策,同時保持對外介面不變。
🧭 Push/Observer → Strategy:這是一對黃金拍檔。記得我們 Day 14 的「城市廣播台」(Observer)嗎?它發布一則
policy.changed
事件,各個服務(例如交通中心)收到後,就能立刻拉取新的策略(Strategy)來應用。
讓我們把鏡頭倒回災難發生前,交通資料工程師 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 端居然要知道現在後台是用哪一套演算法在跑,這太荒謬了!
維運惡夢:臨時為了活動改了邏輯,活動結束後忘了改回來怎麼辦?只能燒香拜佛祈禱了。
一句話概括:我們不把各種演算法寫死,而是把它們各自打包成獨立的「政策包」(策略物件)。交通疏運中心(Context)手上隨時可以持有一個政策包,並且能隨時替換。至於該換成哪個包?這件事交給外部的「政策開關」或我們昨天提到的「城市廣播」事件來決定就好。
✅ 當你有很多種演算法,而且它們需要在執行期間動態切換時。
✅ 當你需要對不同演算法進行 A/B 測試或灰度發布(Canary Release)時。
✅ 當你想把「選擇哪個策略」的邏輯與「策略內部如何實作」的邏輯徹底分離時。
⛔ 如果你的流程是固定的,只是某些步驟的實作不同 ➡️ 那更適合用 Template Method(樣板方法模式)。
⛔ 如果你需要的是協調多個不同組件的複雜互動 ➡️ 那應該是 Mediator(中介者模式) 的主場。
⛔ 如果你的策略只有兩三種,且非常單純 ➡️ 也許一個簡單的函式字典(Dictionary/Map)就夠了,別殺雞用牛刀。
⛔ 常見混淆:State vs. Strategy ➡️ 如果行為的改變是由物件內部狀態自動觸發的(例如紅綠燈從紅燈變綠燈),那是 State 模式;如果行為的改變是由外部客戶端或設定來決定的,那才是 Strategy 模式。
導播,麻煩鏡頭拉一下!讓我們用三個不同的縮放層級,看看這個「政策切換」在 Codetopia 是如何運作的。
① 微觀 (GoF):聚焦在物件之間的合作結構。
② 中觀 (事件驅動):觀察訊息如何在系統中流動。
③ 宏觀 (多代理系統):看更上層的角色如何分工與協商。
視角 | 觀念/模式 | 在 Codetopia 的說法 |
---|---|---|
微觀 (GoF) | Strategy (e.g., RoutingStrategy ) |
「政策包」可隨時插拔;疏運中心 (Context) 負責執行 |
中觀 (EIP/EDA) | Topic policy.changed + Pull 配置 |
Day 14 的廣播台發布通知,各服務主動向「策略庫」拉取新策略 |
宏觀 (MAS) | PolicyAgent 決策、DF (黃頁) 公告可用能力 | 政策代理 (PolicyAgent) 透過黃頁服務 (DF/PolicyRegistry) 發布可用的策略清單 |
Mermaid|類圖 (微觀結構)
Mermaid|時序圖 (中/宏觀資訊流)
這就是 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
從頭到尾都不知道自己現在用的是哪套演算法,它只管執行。這就是分離的藝術!
當心!即使是好的模式也可能被誤用。看到以下情況請立刻鳴笛警告:
策略洩漏 (Leaky Strategy):Context 不小心對外暴露了太多具體策略的細節,導致呼叫端又開始寫 if (context.currentStrategy is RainSafe)
這種判斷,那一切又回到了原點。
狀態肥大化 (Bloated State):把整個城市的即時狀態(像是全域變數)塞進策略物件裡。這會導致策略切換時,舊的狀態可能殘留下來,造成幽靈般的 bug。
策略過度細化 (Strategy Over-Granularity):為了一丁點的差異就建立一個新策略,最後變成「一城百策」,難以管理。缺少版本號或下線機制更是災難。
選擇邏輯寫死 (Hardcoded Selection Logic):用來選擇策略的地方,又變成了一大坨 if-else
。選擇策略的邏輯本身應該由設定檔或外部事件來驅動,而不是寫死在程式碼裡。(我們剛才用 Factory 解決了這個問題!)
如果我們把視角拉得更高,策略模式可以演化成更強大的架構:
Actor 模型:我們可以有一個 PolicyAgent
(政策代理人),它專門監控城市的各項指標(降雨量、人潮密度、交通延遲),在一個 Supervisor
(監督者) 的看管下,平滑地切換交通中心的策略,並向系統回報切換結果。
服務治理:在我們的 DF
(黃頁服務,也就是 PolicyRegistry
)中,可以註冊所有可用的策略及其服務等級目標 (SLO)。廣播台只需要廣播策略的名稱和版本號,各個服務再自己去黃頁拉取對應的策略實作。
兩小時後,Quinn 和 Owen 帶著新架構向市長室回報。讓我們用當時的場景來驗收:
Given: 音樂祭現場人流 6 萬人,晚間 20:00 準時降下雷陣雨。
When: Quinn 透過控制台發布了一條 policy.changed
事件:{"routing":"RainSafe", "fare":"EventOffPeak"}
。
Then:
全市的交通疏導在 15 分鐘內切換完畢,95% 的市民等待疏散時間小於 6 分鐘。
市民手機上的 App 和路邊的智慧看板無需任何更新,就自動顯示出前往避難點的路線和加密後的公車班次。
監控後台顯示,策略切換過程中,系統沒有出現 5xx 錯誤峰值,整體錯誤率低於 0.3%。
相較於活動前一週的交通基線,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
),方便對齊日內熱修、審計與回滾。
這麼漂亮的架構,該怎麼測試呢?
契約測試 (Contract Testing):確保所有 RoutingStrategy
的實作,對於同一個請求,都能回傳語義上等價的 Plan
物件(例如,該有的欄位都有)。
切換測試 (Switching Test):寫一個測試,對 DispatchContext
反覆注入三種不同的策略,然後連續發出 100 次請求,驗證每次的回應都符合當前的策略,沒有任何狀態殘留。
實驗護欄 (Guardrail Test):透過模擬的「城市廣播台」注入假的政策變更事件,驗證線上多個服務實例都能在 1 秒內完成策略的同步更新。
灰度一致性測試 (Grayscale Consistency Test):驗證對於相同的用戶或路段 ID(經過 hash 運算後),在灰度發布期間應始終被分配到同一個策略(新或舊),避免搖擺。
原子切換壓力測試 (Atomic Switch Stress Test):在策略切換的瞬間,模擬大量併發請求(例如每秒 10000 次),確保系統不會回傳混合或不完整的計畫。
Plan
(欄位齊備、safety_score
∈ [0,1]),並確保切換前後不會產生非預期例外。實作挑戰:請為 DispatchContext
增加一個灰度發布功能。例如,讓 10% 的流量走新的 TrafficAware
策略,90% 的流量走舊的 ShortestPath
策略,並記錄兩者的效能差異。你會如何修改 handle
方法?(提示:灰度分流請務必以一個穩定鍵 (如 userId
或路段 ID) 做 hash 運算,以確保同一主體在灰度發布期間能固定命中同一套策略,避免體驗不一致。)
情境二選一:假設臨時暴雨來襲,但只集中在城市西區。身為總設計師的你,會選擇:
A: 全城統一採用 RainSafe
策略(優點:簡單、一致)。
B: 根據地圖格網,只在西區切換為 RainSafe
策略(優點:精準、影響小,但實作複雜)。
請在留言區留下你的選擇 (A/B) 和一句話理由!
今日摘要:策略可插拔,事件驅動切換;同一入口,不同算法,灰度上線穩如老狗。
明天,Codetopia 會遇到一個棘手的市民陳情案,它需要經過好幾個部門的審核。我們要如何打造一條能動態編排、權責分明的處理鏈呢?
明日預告:Day 16|Chain of Responsibility (責任鏈模式) —— 這個案子誰該接?讓它在處理鏈上跑一跑就知道!
為了確保在不支援 Mermaid 渲染的環境中也能正常閱讀,以下提供文中圖表的 ASCII 替代版本:
┌─────────────────────────┐
│ <<<interface>>> │
│ RoutingStrategy │
│─────────────────────────│
│ + route(request): Plan │
└─────────────┬───────────┘
│ implements
┌────────┼────────┐
▼ ▼ ▼
┌──────────┐ ┌──────────┐ ┌──────────┐
│ShortestP.│ │TrafficAw.│ │ RainSafe │
│──────────│ │──────────│ │──────────│
│+route() │ │+route() │ │+route() │
└──────────┘ └──────────┘ └──────────┘
┌─────────────────────────────┐
│ DispatchContext │
│─────────────────────────────│
│ - currentStrategy: Strategy │
│─────────────────────────────│
│ + setStrategy(strategy) │
│ + handle(request): Plan │
└──────────┬──────────────────┘
│ uses
▼
┌─────────────────────────────┐
│ RoutingStrategy │
│ (composition) │
└─────────────────────────────┘
┌─────────┐ ┌─────────────┐ ┌──────────────┐ ┌─────────────┐
│ 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 │
│═══════════════════════════════════════════════════════│
│ Key │ Implementation Class │
│──────────────────────────────────────────────────────│
│ "ShortestPath" │ ShortestPath ──┬──► 🏃♂️ 最短路徑 │
│ "TrafficAware" │ TrafficAware ──┼──► 🚦 即時路況 │
│ "RainSafe" │ RainSafe ──┼──► ☔ 雨天安全 │
│ │ │ │
│ (動態載入...) │ CustomStrategy ──┘──► 🔧 自定義策略 │
└───────────────────────────────────────────────────────┘
▲
│ get(strategyName)
┌────────────────────┴─────────────────────┐
│ on_policy_changed() │
│ ┌─────────────────────────────────────┐ │
│ │ 1. 解析事件參數 │ │
│ │ 2. 查找工廠註冊表 │ │
│ │ 3. 創建策略實例 │ │
│ │ 4. 原子切換 Context 策略 │ │
│ └─────────────────────────────────────┘ │
└───────────────────────────────────────────┘
策略類型 │ 優先考量 │ 適用場景 │ 安全分數
─────────────┼─────────────────┼──────────────────┼─────────
ShortestPath │ 距離最短 │ 日常通勤 │ ★★★☆☆
│ 🎯 效率導向 │ 晴朗天氣 │ (0.60)
─────────────┼─────────────────┼──────────────────┼─────────
TrafficAware │ 即時路況 │ 尖峰時段 │ ★★★★☆
│ 🚦 智能迴避 │ 活動期間 │ (0.75)
─────────────┼─────────────────┼──────────────────┼─────────
RainSafe │ 安全至上 │ 惡劣天氣 │ ★★★★★
│ ☔ 避險優先 │ 緊急疏散 │ (0.95)
初始化階段 執行階段 切換階段
┌──────────┐ ┌──────────┐ ┌──────────┐
│ 策略載入 │───►│ 處理請求 │───►│ 收到切換 │
│ │ │ │ │ 事件 │
│[Factory] │ │[Context] │ │[Event] │
└──────────┘ └─────┬────┘ └─────┬────┘
│ │
▼ ▼
┌──────────┐ ┌──────────┐
│ 執行策略 │ │ 原子切換 │
│ route() │ │ 新策略 │
└─────┬────┘ └─────┬────┘
│ │
▼ ▼
┌──────────┐ ┌──────────┐
│ 回傳計畫 │ │ 繼續服務 │
│ Plan{} │ │ (新策略) │
└──────────┘ └──────────┘
▲ │
│ │
└───────────────┘
(循環處理)
用戶請求流量分配 (基於 Hash 分片)
100% 流量 ┌─────────────────────┐
│ │ Hash Router │
▼ │ (基於 userId) │
┌──────────┐ └─────────┬───────────┘
│ 全量用戶 │ │
│ 請求 │ ▼
└────┬─────┘ ┌─────────────────────┐
│ │ 流量分流決策 │
│ │ hash % 100 │
│ └─────────┬───────────┘
│ │
▼ ▼
┌──────────┐ ┌─────────┐ ┌─────────┐
│ 90% 流量 │ │ 10%流量 │ │ 策略A │
│ 走舊策略 │ OR │ 走新策略│ │ 穩定版 │
│ 📊 監控 │ │ 📈 實驗 │ │ 策略B │
└──────────┘ └─────────┘ │ 測試版 │
└─────────┘
┌─────────────────────────────────────────┐
│ 效能比較與回滾機制 │
│ ┌─────────────┐ ┌─────────────┐ │
│ │ 策略A指標 │ │ 策略B指標 │ │
│ │ 延遲: 50ms │ │ 延遲: 45ms │ │
│ │ 錯誤: 0.1% │ │ 錯誤: 0.3% │ │
│ │ 滿意度: 95% │ │ 滿意度: 97% │ │
│ └─────────────┘ └─────────────┘ │
└─────────────────────────────────────────┘