iT邦幫忙

2025 iThome 鐵人賽

DAY 13
0
Software Development

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

Day 14:Observer:城市廣播、訂閱更新——一呼百應的事件之城

  • 分享至 

  • xImage
  •  

Codetopia 創城記 (14)|Observer:城市廣播、訂閱更新——一呼百應的事件之城

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

話說自從 Day 13,Iris 隊長率隊升級了城門的 Proxy 門禁系統後,Codetopia 的入口總算是固若金湯了。但怪事也跟著來了,城門守住了,城內的訊息卻開始「鬧鬼」。

前端工程師 Dylan 快被逼瘋了,他新設計的地圖 UI,每次有新圖標更新,都得用戶手動 F5 才能看到。「這算什麼智能城市?根本是手搖城市!」他在茶水間抱怨,聲音大到整層樓都聽得見。

另一頭,資產快取暖機器的負責人也一臉茫然,日誌顯示,系統根本沒收到「版本更新」的信號,導致它像個固執的老爺爺(cache warmer 未收到 invalidation signal),不斷回傳過期的舊資料給市民。

而最慘的,莫過於我們可憐的監控工程師 Felix。他的 Pager(一種上古時代的通訊設備)整晚狂響,監控面板上一片爆紅,但他翻遍紀錄,就是找不到同一時間點,到底是哪個環節觸發了這場連鎖災難。

「夠了!」

砰! 一聲巨響,通知平台工程師 Zoe 猛地一拍桌子,震得所有人的咖啡杯都跳了一下。「我們需要的是一個『城市廣播台』!它應該像個插上電就能用的擴音系統——誰變更、誰訂閱、誰就自動更新!」

Zoe 眼中閃爍著不容置疑的光芒,一份可測試、可量測的軍令狀就這樣誕生了:

Feature: City Broadcast SLO (服務等級目標)

  Scenario: Map auto-refresh and cache warm upon icon update
    Given an IconPackUpdated event with id "assets:v3" is ready
    And MapUI, CacheWarmer, and AuditSink are subscribed to the "assets.icon" topic
    When the event is published to the "assets.icon" topic
    Then MapUI handler must complete its redraw operation within 500ms
    And CacheWarmer must schedule its async warm-up task within 1s
    And AuditSink must record the event exactly once, identified by its id
    And a failing observer on the same topic must not block the others from receiving the event

(旁白補充:Zoe 不只定義了功能,更定義了品質。這份軍令狀,不僅是開發指南,更是未來監控告警的鐵律。)

2. 術語卡 🧭

  • Observer (觀察者模式):定義了一種一對多的依賴關係。當一個物件(Subject主體)的狀態發生改變時,所有依賴於它的物件(Observers,觀察者)都會得到通知並自動更新。(註:為避免與 Topic 的「主題」混淆,下文以「主體」稱呼 Subject

  • Push vs. PullPush 指的是主題直接將完整的變更資料推送給觀察者;Pull 則是主題只發送一個提示信號,由觀察者自己決定何時、以及如何去拉取詳細資料。

  • 域內 vs. 跨進程:在同一個應用程式進程內,Observer 模式輕巧好用;但當訊息需要跨越服務或機器的邊界時,請立刻升級你的裝備,改用更專業的 Topic / Pub-Sub 系統。

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

在 Zoe 提出「城市廣播台」這個天才構想前,Codetopia 的通知系統簡直是一場災難。讓我們把時間倒回那個混亂的年代,看看當時的程式碼是怎麼寫的:

# ❌ 反例:到處輪詢 + 到處直呼 = 通知地獄

def map_ui_loop():
    # Dylan 的惡夢:UI 執行緒得自己不斷去問「更新了沒?」
    while True:
        if repo.version() != local.version:   # 輪詢檢查,極度浪費 CPU
            redraw_icons()
        sleep(1)  # 資訊延遲、畫面抖動、市長的電費帳單也在抖動

def on_upload_finish():
    # 上傳模組的詛咒:它必須認識所有關心它的人
    map_ui.redraw()        # 強耦合:直接點名 UI,萬一 UI 改名就崩潰
    cache_warmer.warm()    # 強耦合:又點名快取,新增一個就要改一次
    audit.log("UPDATED")   # 邏輯分散:到底還有誰需要通知?天曉得

# (旁白吐槽:這哪是智慧城市?這根本是個八卦村,村東頭大嬸出門買菜,得親自跑遍全村通知大家。哪天村子大了,大嬸就先累倒在路上了。)

這種寫法的問題顯而易見:

  1. 蜘蛛網般的依賴:系統中形成了 N*N 的直接呼叫關係,牽一髮而動全身。

  2. 順序与重試不可控:如果 map_ui 拋錯,後面的 cache_warmer 就永遠收不到通知了。

  3. 演進與測試的痛苦:每新增一個需要通知的模組,就得去修改 on_upload_finish,違反了開閉原則,測試案例更是越寫越絕望。

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

Observer 模式的核心思想,就是優雅地把「知道有事發生」的權責,和「該怎麼反應」的權責徹底分開。

  • 主體 (Subject):只負責扮演好「城市廣播台」的角色。它的任務清單很簡單:維護一份按主題(Topic)分類的訂閱者名單,並在收到發布請求時,朝著對應主題的聽眾喊一聲:「嘿!有新消息!」。它完全不在乎是誰在聽,也不在乎聽眾們聽到後會做什麼。廣播台不保證同一主題下觀察者的執行順序

  • 觀察者 (Observers):就像是安裝了特定頻道接收器的市民。他們各自決定要如何處理聽到的消息。地圖 UI 選擇重繪畫面,快取選擇更新資料,審計日誌選擇默默記錄。大家各司其職,互不干擾。

何時用 (When to Use) ✅

  • 當一個狀態的改變,需要觸發多個不同的後續行為時。(例如:一次商品上架,需要通知 UI、更新庫存、發送推薦信...)

  • 當你想讓發送端接收端徹底解耦,發送端不需要知道任何具體接收端的細節時。

  • 當你需要一個可動態插拔的通知機制,允許在執行期間隨時增加或移除訂閱者時。

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

  • 跨進程/跨服務通訊:別用!在分散式系統中,你需要更強大的工具,如 RabbitMQ、Kafka 這類的訊息佇列或事件總線。它們提供了持久化、重送保證、順序控制等 Observer 模式沒有的關鍵能力。

  • 需要嚴格的流程編排:如果 A、B、C 三個步驟必須嚴格按照順序執行,且 A 的輸出是 B 的輸入,那這不是廣播,這是工作流。請考慮使用 Mediator (調度中心)Saga 模式 來進行精確的流程控制。

  • 只是想單純疊加功能:如果你只想在一個既有操作前後增加行為(例如:在存檔前後加上日誌),那更適合用輕巧的 Decorator (裝飾者) 模式,而不是引入一整套發布訂閱機制。

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

導播,鏡頭拉一下!讓我們從三個不同維度,看看「城市廣播台」在 Codetopia 的設計圖。

視角 觀念/模式 在 Codetopia 的說法
微觀 (GoF) Subject(主體)維護 subscribe/publish;Observer 實作 update(e) BroadcastCenter 按主題(Topic)發布;MapUI / CacheWarmer / AuditSink 訂閱
中觀 (EIP/EDA) Topic / Pub-Sub、回壓/重送語義 topic: assets.icon.*;可升級訊息匯流排
宏觀 (MAS) BroadcastAgent + SubscriberAgents + DF DF 登錄主題興趣;ACL 契約定義行為

Mermaid|類別圖(微觀結構)

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

Mermaid|時序圖(中觀互動)

https://ithelp.ithome.com.tw/upload/images/20250929/20178500041qQBqO0L.png

6. 最小實作 (英雄的程式碼) 💻

這就是英雄 Zoe 力挽狂瀾,寫下的「城市廣播台」核心程式碼——不僅僅是個範例,而是一個具備主題頻道、併發安全、弱引用防漏、超時監控的準生產級框架

from collections import defaultdict
from typing import Protocol
from collections.abc import Callable
from weakref import WeakSet
import threading
import time
import uuid

# --- 領域事件:定義城市中發生的、可追蹤的、獨一無二的事 ---
class Event:
    def __init__(self, event_type: str, payload: dict | None = None):
        self.event_id = str(uuid.uuid4())  # 每個事件實例都有唯一 ID,用於追蹤與去重
        self.event_type = event_type
        self.payload = payload or {}
        self.timestamp = time.time()
        self.topic: str | None = None      # 由 BroadcastCenter 在 publish() 時注入
    def __repr__(self):
        tp = f", topic={self.topic}" if self.topic else ""
        return f"Event(id={self.event_id}, type={self.event_type}{tp})"

# --- 觀察者合約:使用 Protocol 更具彈性 ---
class Observer(Protocol):
    def update(self, event: Event) -> None: ...

# --- 主題 (Subject):一個工程化的城市廣播台 ---
class BroadcastCenter:
    def __init__(self, handler_timeout_ms: int = 500, on_handler_timing: Callable[[str, str, float], None] | None = None):
        self._lock = threading.RLock() # 可重入鎖,支援複雜場景下的併發安全
        self._subscribers: dict[str, WeakSet] = defaultdict(WeakSet) # 按主題分類,並使用弱引用集合避免內存洩漏
        self._timeout_ms = handler_timeout_ms
        self._on_handler_timing = on_handler_timing  # 可觀測性掛鉤: (topic, obs_name, duration_ms) -> None

    def subscribe(self, topic: str, observer: Observer):
        """訂閱特定主題"""
        with self._lock:
            self._subscribers[topic].add(observer)

    def unsubscribe(self, topic: str, observer: Observer):
        """取消訂閱特定主題"""
        with self._lock:
            if topic in self._subscribers:
                self._subscribers[topic].discard(observer)

    def publish(self, topic: str, event: Event):
        """向特定主題發布事件"""
        with self._lock:
            # 建立訂閱者快照,避免在迭代中修改集合
            targets = list(self._subscribers.get(topic, WeakSet()))

        # 在分派前,把 topic 注入事件,便於觀察者與測試斷言
        if getattr(event, "topic", None) is None:
            event.topic = topic

        for obs in targets:
            start_time = time.time()
            try:
                # 每個觀察者的執行都被保護,一個失敗不影響其他
                obs.update(event)
            except Exception as e:
                print(f"⚠️ 警告: 觀察者 {type(obs).__name__} 處理事件 {event.event_id} 失敗. 錯誤: {e}")
                # 此處可接入真正的告警系統或死信佇列
            finally:
                duration_ms = (time.time() - start_time) * 1000
                if self._on_handler_timing:
                    self._on_handler_timing(topic, type(obs).__name__, duration_ms)
                if duration_ms > self._timeout_ms:
                    print(f"⏱️ 超時警告: 觀察者 {type(obs).__name__} 處理耗時 {duration_ms:.2f}ms, 超出 SLA.")
                    # 此處可接入監控系統,記錄 Handler 性能

# --- 觀察者們 (Observers):實現去重邏輯的審計服務 ---
class AuditSink:
    def __init__(self):
        self._seen_event_ids = set() #  idempotency set,確保事件只被處理一次

    def update(self, event: Event):
        if event.event_id in self._seen_event_ids:
            print(f"📝 AuditSink: 偵測到重複事件 {event.event_id}, 已忽略。")
            return

        print(f"📝 AuditSink: 收到新事件 {event!r}, 已歸檔記錄。")
        self._seen_event_ids.add(event.event_id)
        # 在真實世界,_seen_event_ids 應存於 Redis 或 DB 中以實現跨進程/重啟去重

# (MapUI 和 CacheWarmer 的實現此處從略,專注於框架與去重邏輯)

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

恭喜你,總設計師!你已經掌握了建立「城市廣播台」的藍圖。現在,換你來動手挑戰,並時刻警惕那些可能讓廣播系統變成噪音的「反模式紅旗」。

  1. 實作題:目前的 BroadcastCenter 已經支援了精確的主題訂閱。請你為它增加一個萬用字元訂閱功能。

    • subscribe("assets.*", observer):允許觀察者接收所有以 assets. 開頭的主題事件,例如 assets.iconassets.font

    • 挑戰:你將如何修改 publish 方法的內部邏輯,來高效地匹配萬用字元訂閱者,而不會在每次發布時都遍歷所有訂閱規則?

  2. 思辨題:在廣播事件時,有兩種常見策略:

    • A. Push 模式notify(event) 直接把完整的 event 物件(包含所有變更細節)推送給所有觀察者。

    • B. Pull 模式notify(event_id) 只推送一個輕量的通知或 ID,由觀察者自己決定是否需要、以及何時去向主題或其他服務 pull(拉取)完整的事件詳情。

    如果你是 Zoe,在設計城市廣播台時,你會如何在這兩種模式間取捨?請就以下四個面向,簡述你的考量:

    • 延遲 (Latency):哪種模式的即時性更好?

    • 負載 (Load):哪種模式對「廣播台」和「訂閱者」的負載壓力有何不同?

    • API 演進:當事件的資料結構需要改變時,哪種模式的衝擊面更小?

    • 錯誤恢復:如果觀察者在處理時需要額外資料,哪種模式的彈性更高?

⚡️ 反模式紅旗 (Red Flags) ⚡️

  • 連鎖依賴 (Chain of Updates):觀察者 A 的更新觸發了 B,B 又觸發了 C... 形成一個難以追蹤和除錯的「通知雪崩」。紅旗:如果你發現通知鏈超過一層,立刻停下來,思考是否需要 Mediator 來集中管理協調。

  • 巨型主題 (God Subject):一個廣播台負責發布城市裡所有雞毛蒜皮的事件,從 UI 點擊到資料庫備份。紅旗:當你的 Subject 變得臃腫不堪時,請按照「領域」或「主題」將其拆分成多個小而專的廣播台。

  • 忘記退訂 (Leaky Subscriptions)(我們的 WeakSet 已部分緩解此問題) 但若觀察者被其他地方強引用,仍可能殘留。紅旗:確保在元件的生命週期結束時,一定執行 unsubscribe 邏輯。

  • 同步阻塞 (Blocking Notify):某個觀察者的 update 方法是個超級耗時的長任務。紅旗:我們的超時監控能發現它,但治本之道是將長任務改為非同步執行,或提交到背景任務佇列中處理,避免卡住 publish 迴圈。

8. 測試指北 (Testing Guide) 🧭

一個沒有測試的廣播台,就像一個沒有執照的電台,隨時可能播放災難。以下是確保廣播台穩定運行的幾類關鍵測試(以 pytest 為例):

# 測試用的觀察者樁 (Test Stubs)
class RecordingObserver:
    def __init__(self):
        self.seen = []  # 記錄 (topic, event_type, event_id) 以利斷言
    def update(self, event: Event):
        self.seen.append((event.topic, event.event_type, event.event_id))

class FailingObserver:
    def update(self, event: Event): raise RuntimeError("I failed deliberately!")

# --- 測試案例 ---
def test_subscribe_and_publish_delivers_event():
    """契約測試:訂閱者應能收到發布到其主題的事件。"""
    bc, obs = BroadcastCenter(), RecordingObserver()
    bc.subscribe("ui.clicks", obs)
    ev = Event("ui.clicks")
    bc.publish("ui.clicks", ev)
    assert ("ui.clicks", "ui.clicks", ev.event_id) in obs.seen

def test_unsubscribe_stops_delivery():
    """契約測試:退訂後,不應再收到事件。"""
    bc, obs = BroadcastCenter(), RecordingObserver()
    bc.subscribe("ui.clicks", obs)
    bc.unsubscribe("ui.clicks", obs)
    bc.publish("ui.clicks", Event("ui.clicks"))
    assert obs.seen == []

def test_isolation_on_exception():
    """隔離測試:一個觀察者失敗,不應影響同一主題的其他觀察者。"""
    bc, good_obs, bad_obs = BroadcastCenter(), RecordingObserver(), FailingObserver()
    bc.subscribe("payments", good_obs)
    bc.subscribe("payments", bad_obs)
    ev = Event("payments", payload={"amount": 100})
    bc.publish("payments", ev)
    assert ("payments", "payments", ev.event_id) in good_obs.seen  # 成功的觀察者確實收到

def test_deduplication_in_audit_sink():
    """去重測試:觀察者自身應能處理重複事件。"""
    sink = AuditSink()
    event = Event("audit.login")
    event.event_id = "fixed-id-for-test" # 固定 ID 以模擬重送
    sink.update(event)
    sink.update(event)
    assert len(sink._seen_event_ids) == 1

def test_slo_measurement_hook():
    """SLO 鉤子測試:能正確記錄處理時間。"""
    timings = []
    def timing_collector(topic, obs_name, duration_ms):
        timings.append((topic, obs_name, duration_ms))

    bc = BroadcastCenter(on_handler_timing=timing_collector)
    bc.subscribe("test", RecordingObserver())
    bc.publish("test", Event("test"))

    assert len(timings) == 1
    assert timings[0][0] == "test"
    assert timings[0][1] == "RecordingObserver"
    assert timings[0][2] >= 0

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

今天我們建立的 BroadcastCenter 是一個完美的「同進程」解決方案。但當 Codetopia 發展成一個由多個獨立服務構成的微服務聯邦時,我們的視野也需要隨之提升:

  • EIP/EDA (企業整合模式/事件驅動架構):我們的 BroadcastCenter 會進化成企業級的訊息代理 (Message Broker)。事件會被發布到 Topic 上。事件命名也將遵循更嚴格的規範,例如:
領域 (Domain) 實體 (Entity) 事件 (Event) 完整主題
assets icon updated assets.icon.updated
payments transaction succeeded payments.transaction.succeeded
  • Actor 模型:每個觀察者都可以被視為一個獨立的 Actor,擁有自己的 Mailbox (信箱)。廣播台 BroadcastActor 只需將事件訊息發送給所有訂閱者的信箱,由 Actor 們並行、隔離地處理,天然地解決了同步阻塞和錯誤隔離的問題。

  • 故障剖面:在分散式系統中,我們還需考慮回壓 (Backpressure) 機制(當消費者處理不過來時,如何通知上游減速)和丟棄策略 (Discard Strategy),這些是確保系統韌性的進階話題。

10. 結語 & 預告

Zoe 的廣播台成功上線,軍令狀上的所有驗收項目都亮起了綠燈 ✅。Dylan 的地圖終於能即時更新,Felix 的 Pager 也回歸了久違的寧靜。Codetopia 的訊息傳遞,從混亂的八卦村,一躍成為了高效、可靠的事件之城。

一呼百應拆耦合;誰變更,誰訂閱,誰自動更新。

明日預告:城市廣播系統讓資訊流動順暢,但面對多變的市民需求,市長需要更靈活的政策工具。明天,我們將探討 Day 15|Strategy (策略模式) ——如何做到同一入口,不同算法;讓城市政策像開關一樣自由切換!


12. 附錄:ASCII 版圖示

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

12.1 Observer 模式類別關係圖

                    ┌─────────────────┐
                    │    Subject      │
                    │ (介面/抽象類)    │
                    ├─────────────────┤
                    │ +subscribe()    │
                    │ +unsubscribe()  │
                    │ +publish()      │
                    └─────────┬───────┘
                              │ 實作
                              ▼
                    ┌─────────────────┐
                    │ BroadcastCenter │
                    ├─────────────────┤
                    │ -subscribers    │
                    │ -lock          │
                    │ -timeout_ms    │
                    └─────────┬───────┘
                              │
                              │ 通知 (1對多)
                    ┌─────────▼───────┐
                    │                 │
       ┌────────────▼──┐    ┌────────▼────────┐    ┌────────▼─────┐
       │   Observer    │    │    Observer     │    │  Observer    │
       │  (協議介面)    │    │   (協議介面)     │    │ (協議介面)    │
       │ +update(e)    │    │  +update(e)     │    │ +update(e)   │
       └───────────────┘    └─────────────────┘    └──────────────┘
              ▲                       ▲                      ▲
              │                       │                      │
       ┌──────┴──────┐       ┌───────┴────────┐    ┌────────┴────────┐
       │    MapUI    │       │  CacheWarmer   │    │   AuditSink     │
       │             │       │                │    │                 │
       │ 重繪地圖介面  │       │  預熱快取資料   │    │  紀錄審計日誌    │
       └─────────────┘       └────────────────┘    └─────────────────┘

12.2 事件發布時序流程圖

┌──────────────┐    ┌─────────┐    ┌──────────────┐    ┌─────────────┐    ┌──────────────┐
│              │    │         │    │              │    │             │    │              │
│  Uploader    │    │  Broad  │    │    MapUI     │    │ CacheWarmer │    │  AuditSink   │
│   Module     │    │  Cast   │    │              │    │             │    │              │
│              │    │ Center  │    │              │    │             │    │              │
└──────┬───────┘    └────┬────┘    └──────┬───────┘    └──────┬──────┘    └──────┬───────┘
       │                 │                │                   │                  │
       │ 1. publish()    │                │                   │                  │
       │ topic:          │                │                   │                   │
       │ "assets.icon"   │                │                   │                  │
       │ event: IconUpd  │                │                   │                  │
       ├────────────────▶│                │                   │                  │
       │                 │                │                   │                  │
       │                 │ 2. update(e)   │                   │                  │
       │                 │ [SLA ≤500ms]   │                   │                  │
       │                 ├───────────────▶│                   │                  │
       │                 │                │ ┌─────────────────┐                  │
       │                 │                │ │ 重繪圖示層      │                  │
       │                 │                │ │ 更新UI顯示      │                  │
       │                 │                │ └─────────────────┘                  │
       │                 │                │                   │                  │
       │                 │ 3. update(e)   │                   │                  │
       │                 │ [非同步處理]    │                   │                  │
       │                 ├───────────────────────────────────▶│                  │
       │                 │                │                   │ ┌──────────────┐ │
       │                 │                │                   │ │ 排程預熱任務  │ │
       │                 │                │                   │ │ 更新快取資料  │ │
       │                 │                │                   │ └──────────────┘ │
       │                 │                │                   │                  │
       │                 │ 4. update(e)   │                   │                  │
       │                 │ [記錄日誌]      │                   │                  │
       │                 ├────────────────────────────────────────────────────▶│
       │                 │                │                   │                  │ ┌──────────────┐
       │                 │                │                   │                  │ │ 檢查事件ID   │
       │                 │                │                   │                  │ │ 去重處理     │
       │                 │                │                   │                  │ │ 寫入審計日誌  │
       │                 │                │                   │                  │ └──────────────┘
       │                 │                │                   │                  │
       │ 5. 回傳完成     │                │                   │                  │
       │ ◀──────────────│                │                   │                  │
       │                 │                │                   │                  │

註:所有觀察者並行執行,一個失敗不影響其他,廣播中心提供隔離保護

12.3 城市廣播台系統架構圖

                      🏢 Codetopia 事件驅動城市架構 🏢

                           ┌─────────────────────────┐
                           │     📡 BroadcastCenter   │
                           │      (城市廣播台)         │
                           │                         │
                           │ ┌─────────────────────┐ │
                           │ │  Topic Router       │ │
                           │ │ assets.icon.*       │ │
                           │ │ payments.*          │ │
                           │ │ ui.events.*         │ │
                           │ └─────────────────────┘ │
                           │                         │
                           │ ┌─────────────────────┐ │
                           │ │ WeakSet Subscribers │ │
                           │ │ + 併發安全鎖        │ │
                           │ │ + 超時監控          │ │
                           │ │ + 錯誤隔離          │ │
                           │ └─────────────────────┘ │
                           └─────────┬───────────────┘
                                     │
                    ┌────────────────┼────────────────┐
                    │                │                │
                    ▼                ▼                ▼
           ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
           │  🗺️ MapUI        │ │ ⚡ CacheWarmer  │ │ 📋 AuditSink    │
           │                 │ │                 │ │                 │
           │ ┌─────────────┐ │ │ ┌─────────────┐ │ │ ┌─────────────┐ │
           │ │重繪地圖圖層  │ │ │ │非同步預熱   │ │ │ │事件去重機制 │ │
           │ │更新UI元素   │ │ │ │快取資料     │ │ │ │審計日誌記錄 │ │
           │ │SLA: 500ms  │ │ │ │背景任務排程 │ │ │ │持久化存儲   │ │
           │ └─────────────┘ │ │ └─────────────┘ │ │ └─────────────┘ │
           └─────────────────┘ └─────────────────┘ └─────────────────┘

        ┌─────────────────────────────────────────────────────────────────┐
        │                        事件生命週期                               │
        │                                                                 │
        │ Event{                                                          │
        │   id: "uuid-1234",           👈 唯一識別,支援去重                │
        │   type: "IconPackUpdated",   👈 事件類型,便於分類處理            │
        │   payload: {...},            👈 業務資料載荷                      │
        │   timestamp: 1640995200,     👈 時間戳,支援排序與過期            │
        │   topic: "assets.icon"       👈 主題路由,由廣播台注入            │
        │ }                                                               │
        └─────────────────────────────────────────────────────────────────┘

    🔧 運維監控儀表板
    ┌───────────────┬───────────────┬───────────────┬───────────────┐
    │ 📊 主題流量    │ ⏱️  處理延遲   │ 🚨 錯誤率     │ 💾 訂閱者數量  │
    │ assets.*: 1.2K│ MapUI: 120ms  │ Global: 0.1% │ Total: 156    │
    │ payments: 890 │ Cache: 2.3s   │ Cache: 0.3%  │ Active: 142   │
    │ ui.events: 3.4K│ Audit: 45ms   │ Audit: 0.0%  │ Failed: 14    │
    └───────────────┴───────────────┴───────────────┴───────────────┘

12.4 觀察者模式的演進路徑

    演進階段                    適用場景                     技術實現

┌─────────────┐    ┌─────────────────────────┐    ┌─────────────────────┐
│ 🏠 Stage 1   │    │ 單一進程內的元件通信      │    │ • Subject/Observer  │
│  In-Memory  │ ──▶│ • 同一應用程式內        │ ──▶│ • WeakSet 管理     │
│  Observer   │    │ • 延遲要求較低          │    │ • 同步/非同步混合   │
└─────────────┘    └─────────────────────────┘    └─────────────────────┘
       │                                                    │
       ▼                                                    ▼
┌─────────────┐    ┌─────────────────────────┐    ┌─────────────────────┐
│ 🌐 Stage 2   │    │ 跨服務/跨進程通信        │    │ • Message Broker   │
│ Distributed │ ──▶│ • 微服務架構            │ ──▶│ • Topic-based      │
│ Pub/Sub     │    │ • 需要持久化與重送      │    │ • RabbitMQ/Kafka   │
└─────────────┘    └─────────────────────────┘    └─────────────────────┘
       │                                                    │
       ▼                                                    ▼
┌─────────────┐    ┌─────────────────────────┐    ┌─────────────────────┐
│ 🎭 Stage 3   │    │ 複雜事件流處理          │    │ • Event Sourcing   │
│ Event       │ ──▶│ • CQRS 架構            │ ──▶│ • Stream Processing│
│ Sourcing    │    │ • 事件回溯與重播        │    │ • Apache Kafka     │
└─────────────┘    └─────────────────────────┘    └─────────────────────┘

💡 選擇指南:
   • 從最簡單的 In-Memory 開始
   • 當需要跨進程時,升級到 Message Broker
   • 當需要事件回溯時,考慮 Event Sourcing

上一篇
Day 13:Proxy:資產大門的警衛——該放行?該攔截?還是遠端代打?
系列文
Codetopia 新手日記:設計模式與原則的 30 天學習之旅14
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言