例外聚合(exception aggregation):用一份程式碼處理大量例外,而不是為每個例外寫個別 handler。

範例:Web 伺服器的缺漏參數處理#

Web 伺服器的標準作法:

  • 接到 URL → 分派器(dispatcher)→ 對應的服務方法
  • 服務方法呼叫低層 getParameter 抽取所需參數
  • 參數不存在 → 拋例外

反例:每個 getParameter 都包 try/catch#

學生實作時的常見錯誤:

service method 1
    try { getParameter("a") } catch (NoSuchParameter) { 回應錯誤 }
    try { getParameter("b") } catch (NoSuchParameter) { 回應錯誤 }
    ...
service method 2
    ...(同樣的處理重複 N 次)

每個 handler 做的事差不多(產生錯誤回應),卻散落各處。

Figure 10.1: dispatcher 把每個 URL 分派到對應的方法;每個 getParameter 各自包 try/catch,造成程式碼重複

改善:在 dispatcher 層次集中處理#

讓例外向上傳到 dispatcher,單一 handler 處理所有缺漏參數:

dispatcher
    try { 分派並執行 service method } catch (NoSuchParameter) { 回應錯誤 }

Figure 10.2: 與 Figure 10.1 功能等價,但例外處理被聚合——dispatcher 中單一 handler 處理所有 NoSuchParameter 例外

進一步擴張聚合範圍#

Web 處理過程中可能發生許多錯誤:

  • 缺參數
  • 參數語法錯誤(期待整數卻收到 xyz
  • 使用者沒有權限

每種錯誤其實只是「回傳不同錯誤訊息」。可以:

  • 把錯誤訊息塞進例外裡
  • 例如 getParameter 拋出時帶訊息 parameter 'quantity' not present in URL
  • 頂層 handler 從例外取出訊息、組裝錯誤回應

為什麼這個聚合對封裝友善#

部分知道的事不知道的事
頂層 handler怎麼生成錯誤回應具體錯誤是什麼
getParameter怎麼從 URL 抽參數、怎麼用人話描述抽取錯誤HTTP 錯誤回應的語法

兩塊知識各自封裝;新功能(新的 getParameter 類方法)只要拋同祖類的例外、附帶訊息,頂層 handler 就會自動處理

通用 design pattern#

系統處理一連串請求時,可以定義一個例外,能夠:

  • 中止當前請求
  • 清理系統狀態
  • 繼續處理下一個請求

在請求迴圈頂端用單一處抓這個例外即可。

不同錯誤條件用不同子類,但都共享頂層處理。這類例外要與「整個系統致命」的例外清楚區分

聚合 vs. 屏蔽:方向相反#

技巧處理位置為何效果好
屏蔽低層(library 方法)庫方法被多處呼叫,在低層攔截可避免每個呼叫端各自處理
聚合高層(請求迴圈)例外向上傳播多層,單一處可吸收許多 method 的例外

兩者本質相似——都把處理碼放在能抓到最多例外的位置。

範例:RAMCloud 的「錯誤升級」#

RAMCloud 是一個分散式儲存系統:

  • 多副本機制讓系統能從各種失敗中復原
  • 若伺服器當機並丟資料 → 從其他伺服器副本重建

設計取捨#

RAMCloud 為每種錯誤寫獨立的恢復機制。

實際做法:把許多較小的錯誤升級成較大的錯誤。

  • 偵測到單一物件損壞時 → 崩潰整台伺服器(而不是只還原該物件)
  • 為什麼?崩潰恢復本來就要做、又很複雜,重用同一機制大幅減少要寫的程式碼
  • 額外好處:恢復機制被觸發更頻繁 → 它的 bug 更容易被發現與修掉

代價#

把單一物件損壞升級成伺服器崩潰,恢復成本顯著上升

在 RAMCloud 中可接受,因為物件損壞極罕見。

但對頻繁發生的錯誤就不適合:例如不能每次封包遺失就崩潰整台伺服器。

一個觀察角度#

例外聚合相當於用一個通用機制取代多個專用機制

這也是「通用機制更深」原則的展現。