Codetopia 的年度晚會即將拉開序幕,內容策展英雄 Mira 和整合工程師 Ken 正在做最後的系統檢查。
就在倒數十分鐘,採購專員 Mia 像一陣風一樣衝進了控制室,遞來一份剛生效的資安條款:「緊急狀況!所有對外公告,現在開始都必須加上數位簽名!部分內容需要加密,而且有些嘉賓資訊,必須在晚上九點後才能顯示!」
Ken 的眉頭鎖得更深了。現有的 SingleNotice
和 NoticeGroup
是城市資訊系統的基石,穩定運行了數個月,總設計師早就下令:「核心類別,一個字都不准動!」
Mira 看著既有的 publish
流程,腦中一片混亂。難道要在最後關頭,用一堆 if-else
來硬幹嗎?這不僅是技術災難,更是對優雅架構的背叛。時間,分秒必爭。如何在不修改核心程式碼的前提下,為既有的公告物件「穿上」這些新功能?💡
Decorator(裝飾者):以相同介面包裝目標物,在不改動原類別的前提下,動態地為其疊加行為與責任。
Middleware / Filter(中介軟體/過濾器):在企業整合模式(EIP)或事件驅動架構(EDA)中,對應於一連串可組合的前後置處理器,形成裝飾鏈。
Wrapper Agent(包裝代理):在多代理系統(MAS)中,包裹既有代理以擴充其能力,例如加上簽名、節流或審計功能。
面對壓力,Mira 決定先用「最直覺」的方式來解決問題。「不就是多了幾個條件嗎?加幾個旗標就好了!」
於是,一段充滿「味道」的程式碼誕生了:
# 用於示範的簡化反例
from datetime import datetime
# ❌ 反例:旗標地獄 + 違反開放封閉原則
def publish_notice(notice, driver, encrypt=False, sign=False, after9pm=False):
# 違反單一職責,把所有邏輯混在一起
if after9pm and datetime.now().hour < 21:
print("DEBUG: 時間未到,公告被悄悄吞掉...")
return
payload = {"title": notice.title, "body": notice.body}
if sign:
# 簽名邏輯寫死在此
payload["sig"] = naive_sign(payload)
if encrypt:
# 加密邏輯也寫死在此
payload["body"] = cheap_encrypt(payload["body"])
driver.send(payload) # 明天如果要加上「壓縮」功能?那只好再來一個 if ...
砰! 這段程式碼剛寫完,Ken 就忍不住拍了桌子。「Mira,妳這是要把我們推進火坑啊!」
問題點一:違反開放封閉原則。每增加一個新功能(壓縮、追蹤 ID),都必須回來修改這個核心函式,風險極高。
問題點二:組合與順序災難。「先簽名再加密」和「先加密再簽名」的結果天差地遠,用旗標根本無法靈活調整順序。
問題點三:測試地獄。要測試所有旗標的組合,根本是一場永無止境的噩夢。
這條路,顯然是死胡同。它完美展示了旗標地獄、子類爆炸、順序災難等問題——而這些,恰好都是 Decorator 模式專治的症狀。⚠️
就在兩人一籌莫展之際,總設計師的身影出現在門口。他看了一眼程式碼,笑著說:「別慌,我們不是要修改房子,只是需要幫它做點『裝修』。」
這就是 Decorator(裝飾者)模式 的核心思想:用一個擁有相同介面的物件,去包裹(wrap)另一個物件,從而在不改變其原始結構的情況下,疊加上新的能力。 就像俄羅斯娃娃一樣,一層套一層,每一層都增加一點新功能。
✅ 想在不修改原始類別的情況下,為物件動態添加功能時。特別適用於非功能性需求(NFR),如日誌、快取、權限、簽名、加密等。
✅ 當這些功能可以自由組合,且組合的順序很重要時。(例如:先壓縮 -> 再加密 -> 最後簽名)。
✅ 當你需要逐步為一個簡單的物件增加複雜度,同時又想避免產生大量功能混雜的子類時。
⛔ 需求是根本性的介面轉換或型別變更 → 這時候你該先考慮 Adapter(轉接器) 模式,它的職責是「翻譯」介面,而非「增強」功能。
⛔ 需求是控制物件的存取、延遲載入或遠端代理 → Proxy(代理) 模式更貼切,它關注的是「控制」而非「裝飾」。
⛔ 只需要一個簡單、全域性的行為,且不需要動態組合 → 可能用 Facade(外觀) 統一入口,或在 Pipeline 中加入一個固定步驟就足夠了,不必動用 Decorator 這麼靈活的結構。
導播,鏡頭拉一下!讓我們從三個不同的視角來看看「裝修」這件事在 Codetopia 是如何運作的。
視角 | 觀念/模式 | 在 Codetopia 的說法 |
---|---|---|
微觀 (GoF) | Component / ConcreteComponent / Decorator / ConcreteDecorator | NoticeUnit / SingleNotice / NoticeDecorator / (Sign/Encrypt/After9pm) |
微觀 (GoF) | Component / ConcreteComponent / Decorator / ConcreteDecorator | NoticeUnit / SingleNotice / NoticeDecorator / (Sign/Encrypt/After9pm) |
中觀 (EIP/EDA) | Filter/Middleware Chain (過濾器/中介軟體鏈) | 簽名 → 加密 → 壓縮 → 重試 的處理管線 |
宏觀 (MAS) | Wrapper Agent (包裝代理) | 一個「審計代理」包裹著一個「投放代理」,來記錄所有發送的訊息 |
這張圖揭示了 Decorator 的核心結構:裝飾者 (NoticeDecorator
) 和被裝飾者 (SingleNotice
) 都繼承自同一個介面 (NoticeUnit
),並且裝飾者內部還會「持有」一個被裝飾的物件。
這張圖展示了一條由各種 Driver 裝飾器組成的處理鏈。請求從最外層的裝飾器開始,一路向內傳遞,直到最終的 ConsoleDriver
。
「看好了,」總設計師邊說邊在白板上寫下了程式碼,「我們可以兵分兩路來裝飾:一路裝飾『通路』(Driver),負責 I/O 和安全;另一路裝飾『內容』(Notice),負責業務邏輯。」
# === 1. 共用協定與環境設定 ===
import hmac, hashlib, os, json, base64
from typing import Protocol, Dict, Any, Optional, Callable, runtime_checkable
from datetime import datetime
from zoneinfo import ZoneInfo
# 從環境變數讀取密鑰,提供一個預設值
SECRET_KEY = os.environ.get("APP_HMAC_KEY", "default-secret-key-for-demo").encode()
# === 2. Driver 相關定義 ===
@runtime_checkable
class DisplayDriver(Protocol):
def send(self, payload: Dict[str, Any]) -> None: ...
class ConsoleDriver:
def send(self, payload: Dict[str, Any]) -> None:
print(f"[SINK] >> {json.dumps(payload, ensure_ascii=False)}")
class DriverDecorator(DisplayDriver):
def __init__(self, inner: DisplayDriver): self.inner = inner
def send(self, payload: Dict[str, Any]) -> None: self.inner.send(payload)
class SignDriver(DriverDecorator):
"""簽名裝飾器:使用 HMAC-SHA256 計算簽名"""
def send(self, payload: Dict[str, Any]) -> None:
# 註:簽名計算基於「加入 sig 前」的 payload。
# 若在 SignDriver 之後還會增刪欄位(不建議),需將簽名移至最外層。
signature = hmac.new(SECRET_KEY, json.dumps(payload, sort_keys=True).encode(), hashlib.sha256).hexdigest()
signed_payload = dict(payload, sig=signature)
super().send(signed_payload)
class EncryptDriver(DriverDecorator):
"""加密裝飾器:(範例) 將 body 內容用 Base64 編碼"""
def send(self, payload: Dict[str, Any]) -> None:
encrypted_payload = dict(payload)
if "body" in encrypted_payload and isinstance(encrypted_payload["body"], str):
encrypted_payload["body"] = base64.b64encode(encrypted_payload["body"].encode()).decode()
encrypted_payload["enc"] = "b64" # 註記演算法
super().send(encrypted_payload)
class RetryDriver(DriverDecorator):
"""重試裝飾器:當發生錯誤時,最多重試指定次數"""
def __init__(self, inner: DisplayDriver, times: int = 3):
super().__init__(inner)
self.times = times
def send(self, payload: Dict[str, Any]) -> None:
last_error: Optional[Exception] = None
for attempt in range(self.times):
try:
print(f"[Decorator Check] >> Attempt {attempt + 1}/{self.times}...")
return super().send(payload)
except Exception as e:
last_error = e
raise last_error or RuntimeError("Send failed after multiple retries")
# === 3. Notice 相關定義 ===
class NoticeUnit(Protocol):
def publish(self, driver: DisplayDriver) -> None: ...
class SingleNotice:
def __init__(self, title: str, body: str): self.title, self.body = title, body
def publish(self, driver: DisplayDriver) -> None: driver.send({"title": self.title, "body": self.body})
class NoticeDecorator(NoticeUnit):
def __init__(self, inner: NoticeUnit): self.inner = inner
def publish(self, driver: DisplayDriver) -> None: self.inner.publish(driver)
class After9pm(NoticeDecorator):
"""時間限制裝飾器:注入時鐘以利測試"""
def __init__(self, inner: NoticeUnit, clock: Callable[[], datetime] = lambda: datetime.now(ZoneInfo("Asia/Taipei"))):
super().__init__(inner)
self.clock = clock
def publish(self, driver: DisplayDriver) -> None:
if self.clock().hour >= 21:
print("[Decorator Check] >> After 9 PM, proceeding...")
self.inner.publish(driver)
else:
print("[Decorator Check] >> Before 9 PM, skipped.")
driver.send({"status": "skipped", "reason": "time_limit"})
# --- 4. 使用場景:自由組合 ---
basic_notice = SingleNotice("舞台 A 更新", "嘉賓加碼一首安可曲")
# 需求一:廣場大螢幕,先加密、再簽名,並帶有重試機制
# 關鍵:裝飾器「由外向內」執行。最外層的 RetryDriver 會先執行。
square_driver = RetryDriver(EncryptDriver(SignDriver(ConsoleDriver())), times=3)
# 需求二:App 推播,只需簽名,且 21:00 後才放行
app_driver = SignDriver(ConsoleDriver())
test_time_before = datetime(2025, 9, 24, 20, 59, tzinfo=ZoneInfo("Asia/Taipei"))
test_time_after = datetime(2025, 9, 24, 21, 0, tzinfo=ZoneInfo("Asia/Taipei"))
app_notice_before = After9pm(basic_notice, clock=lambda: test_time_before)
app_notice_after = After9pm(basic_notice, clock=lambda: test_time_after)
print("--- 模擬發送到「廣場大螢幕」(重試->加密->簽名)---")
basic_notice.publish(square_driver)
print("\n--- 模擬發送到「App」(20:59,應被跳過)---")
app_notice_before.publish(app_driver)
print("\n--- 模擬發送到「App」(21:00,應成功發送)---")
app_notice_after.publish(app_driver)
🔒 Security Note
本文中的程式碼範例主要用於教學,旨在闡明 Decorator 模式的結構。
加密:Base64 是一種編碼而非加密,無法保護資料機密性。在真實世界的應用中,請務必使用如
AES-GCM
或libsodium
等經過驗證的對稱加密函式庫。簽名:範例中的
HMAC-SHA256
是確保訊息完整性與來源驗證的良好起點。在某些場景下,也可以考慮使用如Ed25519
等數位簽章演算法。產線建議:實務上,建議的處理順序為
Encrypt -> Sign
(簽署加密後的密文)。金鑰與 IV 應透過 KMS 等金鑰管理服務管理;伺服器端應在解析內容前,優先驗證簽章。
EIP/EDA (中觀):Decorator 模式的思想,完美對應到訊息處理中的 Filter/Middleware Chain。每一個 Decorator 就像是管線 (Pipeline) 中的一個過濾器,對流經的訊息進行加工。
MAS (宏觀):在多代理系統中,我們可以用 Wrapper Agent 來擴充既有代理的功能。例如,一個普通的「任務執行代理」可以被一個「審計包裝代理」包裹,後者會記錄所有執行的任務和結果。
部署位置:像 Driver
這樣的裝飾器鏈,可以靈活地部署在發布者(Publisher)端,也可以部署在通道配接器(Channel Adapter)上,取決於你的架構設計。
看著新架構,Ken 迅速完成了部署。
Given: 既有的 SingleNotice
和 ConsoleDriver
,程式碼完全未被修改。
When:
為「廣場大螢幕」的通路組合 RetryDriver(EncryptDriver(SignDriver(ConsoleDriver())))
。
為「App 推播」的內容組合 After9pm(SingleNotice(...), clock=...)
。
Then:
所有新功能都透過裝飾器疊加上去,並且可以任意重排順序。
廣場螢幕:公告內容先被加密,然後整個加密後的 payload 被簽名,最後由重試機制包裹,符合資安與穩定性要求。
App 推播:在 20:59 時,公告被 After9pm
裝飾器攔截並回報 skipped
;調整時鐘到 21:00 後,公告成功發布。
未來若要新增「壓縮」功能,只需再寫一個 CompressionDriver
並插入鏈中即可。
晚會的時鐘敲響,第一則公告準時出現在大螢幕上。危機解除!✅
一個可擴展的裝飾器架構,必須有穩固的測試來保護。
契約測試 (Contract):確保每一個 Decorator 都嚴格遵守 DisplayDriver
協定。若要讓 isinstance
能在執行期檢查 Protocol,記得加上 @runtime_checkable
裝飾器;否則,應依賴靜態型別檢查(如 mypy)或 Duck Typing 來驗證行為。
順序測試 (Order):要驗證「先加密再簽名」的順序是否正確,可以建立一個 TraceDriver
來收集歷程,然後斷言 payload 的狀態。
from typing import Dict, Any
class TraceDriver(ConsoleDriver):
def __init__(self): self.history = []
def send(self, payload: Dict[str, Any]):
self.history.append(payload.copy())
super().send(payload)
# 測試
trace_driver = TraceDriver()
EncryptDriver(SignDriver(trace_driver)).send({"body": "secret"})
# sink_payload 是最終傳遞到最內層 Driver 的物件
sink_payload = trace_driver.history[-1]
# 斷言 sink_payload 包含 'enc' 和 'sig' 兩個鍵
時間測試 (Time):對於像 After9pm
這樣與時間相關的邏輯,務必注入時鐘!這樣你才能在測試中固定時間,分別斷言 20:59(跳過)和 21:00(放行)的行為。
韌性測試 (Resilience):測試 RetryDriver
時,可以注入一個會在前兩次呼叫時拋出異常的假 Driver,並断言最终呼叫成功。
Mira 和 Ken 看著白板上的程式碼,恍然大悟。原來功能可以像積木一樣疊加上去!現在,換你來當一次 Codetopia 的英雄了。
RateLimitDriver:請你實作一個 RateLimitDriver(limit_per_min)
裝飾器。當呼叫頻率超過設定的限制時,就暫緩發送或直接丟棄請求。
反思與比較:試著把「簽名」功能從 Driver
層搬到 Notice
層,做成 SignedNotice(notice)
這樣的裝飾器。然後比較一下,把簽名這個責任放在「內容層」和「通路層」,各自有什麼優缺點?
旗標地獄:在單一流程中用 if encrypt / if sign
來控制行為,是 Decorator 模式要極力避免的。
子類爆炸:如果為每種功能的組合都創建一個新的子類(例如 SignedAndEncryptedNotice
),那類別的數量將會失控。
裝飾順序不明:團隊必須清楚記錄或透過測試來確保「先密後簽」與「先簽後密」的行為符合預期,否則會埋下安全隱憂。
假設現在要新增一個「A/B 測試標籤」的功能,你會把它實作在哪一層?
A. Driver Decorator (通路層,所有經過的 payload 都會被一致處理)
B. Notice Decorator (內容層,更靠近資料的語意,可以針對特定內容加標籤)
在留言區留下你的選擇和一句理由吧!
不動原始物,用裝飾器疊功能;順序可換,能力可疊。
今天,我們學會了如何在不驚動既有程式碼的情況下,為其穿上華麗的新衣。但當系統內部越來越複雜,有數十個子系統需要協調時,我們又該如何提供一個簡單、統一的入口呢?
明日預告:Day 11|Facade:為複雜的城市內部系統,打造一個親民的「一站式服務窗口」!
為了確保在不支援 Mermaid 渲染的環境中也能正常閱讀,以下提供文中圖表的 ASCII 替代版本:
+----------------+ +------------------+
| <<interface>> | | SingleNotice |
| NoticeUnit |<-----| +publish() |
| +publish() | +------------------+
+----------------+
^
|
+----------------+ +------------------+
| NoticeDecorator|----->| NoticeUnit |
| +publish() |wraps +------------------+
+----------------+
^ ^ ^
| | |
| | |
+---+ +--+ +-+-------+
| | | |
v v v v
+------+ +--+--+ +---+-----+
|After9pm| |Sign | |Encrypt |
+--------+ +-----+ +---------+
Client RetryDriver EncryptDriver SignDriver ConsoleDriver
| | | | |
|--send()----->| | | |
| |--send()------>| | |
| |(retry logic) | | |
| | |--send()------>| |
| | |(encryption) | |
| | | |--send()------->|
| | | |(signing) |
| | | | |(final output)
|<----ack------|<--------------|--------------<|<---------------|
| | | | |
+---------------+
| |
| BasicNotice |
| |
+-------+-------+
^
| wrapped by
+--------+--------+
| |
| After9pm |
| |
+-----------------+
+-----------------+ +-----------------+ +-----------------+ +-----------------+
| | | | | | | |
| ConsoleDriver |<----| SignDriver |<----| EncryptDriver |<----| RetryDriver |
| | | | | | | |
+-----------------+ +-----------------+ +-----------------+ +-----------------+
^ ^ ^ ^
| | | |
| | | |
Inner Driver Inner Driver Inner Driver Outermost Driver
(最內層執行) (第二層執行) (第三層執行) (最外層先執行)