介面設計是系統架構的基石,直接決定了 API 的易用性、安全性與演進空間。本文深入探討兩大核心設計原則——命令查詢分離(CQS)與組合方法,並通過 Iterator、EventSource、Resource 等經典案例,揭示在單一職責與使用便利性之間如何做出明智權衡。
優秀的介面需平衡三個維度:
封裝的本質不僅是資料隱藏,更是「自包含」:類應自行處理同步、順序依賴、資源管理等細節,而非推給呼叫端。
| 類型 | 特徵 | 範例 |
|---|---|---|
| 查詢 | 返回值、無副作用、冪等 | hasNext()、size()、getName() |
| 命令 | 改變狀態、無返回值(void) | remove()、add()、setName() |
public interface Iterator {
boolean hasNext(); // 純查詢
Object next(); // 混合:查詢當前元素 + 命令移動游標
void remove(); // 純命令
}
為何違反 CQS? 若拆分為 current()(查詢)+ advance()(命令),客戶代碼變為:
// 複雜且易出錯
for (iter.init(); iter.hasNext(); iter.advance()) {
Object current = iter.current();
// use current
}
結論:next() 合併命令與查詢,雖降低理論純度,但大幅提升使用便利性,是務實的妥協。
// EventSource 介面
interface EventSource {
Handler getHandler(Event event); // 查詢
void installHandler(Event event, Handler newHandler); // 命令
}
// 客戶代碼:看似正確,實則有競態條件
Handler oldHandler = eventSource.getHandler(event); // 步驟 1
// 此處可能被其他線程修改 handler
eventSource.installHandler(event, newHandler); // 步驟 2
同步塊無法救援:若 eventSource 是遠端代理,synchronized(eventSource) 鎖住的是代理物件而非真實目標,無法保證原子性。
// 合併為單一原子操作
interface EventSource {
Handler installHandler(Event event, Handler newHandler); // 返回舊 handler
}
// 客戶代碼:簡潔、線程安全、分散式透明
Handler oldHandler = eventSource.installHandler(event, newHandler);
優勢:
// 分離設計:併發下不可靠
interface Resource {
boolean isAcquired(); // 查詢
void acquire(); // 命令
void release();
}
// 組合方法:tryAcquire 封裝「檢查-獲取」原子操作
interface Resource {
boolean tryAcquire(); // 原子性:成功返回 true,失敗返回 false
void release();
}
// 使用極簡
if (resource.tryAcquire()) {
// 獲取成功
}
| 場景 | 推薦模式 | 理由 |
|---|---|---|
| 單執行緒、本地記憶體操作 | CQS 純分離 | 理論純度高,利於斷言式編程與測試 |
| 多執行緒共享可變狀態 | 組合命令方法 | 封裝同步細節,避免呼叫端競態條件 |
| 分散式/遠端呼叫 | 組合方法(含命令+查詢) | 減少網路往返,解決代理同步失效問題 |
| 高頻迭代/遍歷場景 | 合併 next()(查詢+命令) | 符合使用習慣,代碼簡潔 |
| 需支援事務語義(全成功/全失敗) | 組合方法 | 原子性邊界清晰 |
介面設計無絕對標準,只有情境化的權衡。CQS 提供理論基準,組合方法解決現實骨幹。優秀的 API 設計師懂得:在正確的地方打破純粹性,換取系統整體的正確性與簡潔性。這正是「介面設計藝術」的精髓所在。