例外聚合(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 中可接受,因為物件損壞極罕見。
但對頻繁發生的錯誤就不適合:例如不能每次封包遺失就崩潰整台伺服器。
一個觀察角度#
例外聚合相當於用一個通用機制取代多個專用機制。
這也是「通用機制更深」原則的展現。