摘要
我們在設計系統接口時
經常會遇到這樣的問題
我們的接口應該提供多少方法才合適?
我們的接口應該提供
原子方法
還是
復合方法
?
我們的接口是否應該封裝(或者
能否封裝)所有的細節?
接口的設計需要考慮用戶的使用習慣
使用的方便程度
使用的安全程度
根據我的編程經驗
下面會詳細討論接口設計的
個需要權衡的方面
接口的單一化 & 復合化
接口
接口提供了不同系統之間或者系統不同組件之間的界定
在軟件中
接口提供了一個屏障
從而從實現中分離目標
從具體中分離抽象
從作者中分離用戶
站在用戶的角度看
一個接口建立並命名了一個目標對象的使用方法
一些約束(例如
編譯時的類型系統
運行時的異常機制及返回值)使得類作者的目的得以體現和加強
供給(affordances)指事物的被感知的真實的屬性
這些屬性可以決定事物使用的可能方法
供給提供了對事物操作的線索
類設計者的一個職責便是在接口中減小約束與供給之間的隔閡
匹配目標以及一定程度上的自由度
盡可能減小錯誤使用目標對象的可能
封裝
對於封裝來說
遠不止數據私有那麼簡單
在設計中
封裝往往會涉及到自我包含(self
containment)
如果一個類需要你知道如何調用它方法(e
g
在一個線程的環境中
在一個方法調用後調用另一個方法
你必須明確地同步對象)
那麼它的封裝性就不如將所有這些全部包含並隱藏的類(e
g
這個類是thread
safe的)好
前一個設計存在著設計的漏洞
它的許多限定條件是模糊的
而且把部分責任推給了用戶
而不是讓類提供者做這些工作來完成類的設計
在空間或者時間上分離方法的執行(例如
線程
遠程方法調用
消息隊列)
能夠對設計的正確性和效率產生意義深遠的影響
這種分離帶來的結果是不可忽視的
並發引入了不確定性和環境(context)選擇的開銷
分布引入了回調的開銷
這些開銷可能不斷增加
而且會導致錯誤
這些是設計的問題
修改它們可不是象修改bug那樣簡單
如果一個接口主要由存取方法(set和get方法)組成
每個方法都相應的直接指向某個私有域
那麼它的封裝性會很差
接口中的域存取方法通常是不會提供信息的
他們在對象的使用中不能通訊
簡單化和抽象化
這通常會導致代碼冗長
並且容易出錯
所以
我們首先考慮接口設計的第一個原則
命令與查詢分離(Command
Query Separation)
要求
保證一個方法不是命令(Command)就是查詢(Query)
定義
查詢
當一個方法返回一個值來回應一個問題的時候
它就具有查詢的性質
命令
當一個方法要改變對象的狀態的時候
它就具有命令的性質
通常
一個方法可能是純的Command模式或者是純的Query模式
或者是兩者的混合體
在設計接口時
如果可能
應該盡量使接口單一化
保證方法的行為嚴格的是命令或者是查詢
這樣查詢方法不會改變對象的狀態
沒有副作用(side effects)
而會改變對象的狀態的方法不可能有返回值
也就是說
如果我們要問一個問題
那麼就不應該影響到它的答案
實際應用
要視具體情況而定
語義的清晰性和使用的簡單性之間需要權衡
例如
在java
util
Iterator中
hasNext可以被看作一種查詢
remove是一種命令
next合並了命令和查詢
public interface Iterator{
boolean hasNext();
Object next();
void remove();
}
這裡
如果不將一個Iterator對象的當前值向前到下一個的話
就不能夠查詢一個Iterator對象
如果沒有提供一個復合方法next
我們將需要定義一系列的命令方法
例如
初始化(initialization)
繼續(continuation)
訪問(access)和前進(advance)
它們雖然清晰定義了每個動作
但是
客戶代碼過於復雜
for(initialization; continuation condition; advance){
access for use
}
將Command和Query功能合並入一個方法
方便了客戶的使用
但是
降低了清晰性
而且
可能不便於基於斷言的程序設計並且需要一個變量來保存查詢結果
Iterator iterator = erator();
while(iterator
hasNext();){
Object current = iterator
next();
use current
}
下面
我們考慮接口設計的第二個原則
組合方法(Combined Method)
組合方法經常在線程和分布環境中使用
來保證正確性並改善效率
一些接口提供大量的方法
起初
這些方法看來是最小化的
而且相關性強
然而
在使用的過程中
一些接口顯現得過於原始
它們過於簡單化
從而迫使類用戶用更多的工作來實現普通的任務
並且
方法之間的先後順序及依賴性比較強(即
暫時耦合)
這導致了代碼重復
而且非常麻煩和容易出錯
一些需要同時執行成功的方法
在多線程
異常
和分布的情況下會遇到麻煩
如果兩個動作需要同時執行
它們由兩個獨立的方法進行描述
必須都完全成功的執行
否則會導致所有動作的回滾
線程的引入使這種不確定性大大增加
一系列方法同時調用一個易變的(mutable)對象
如果這個對象在線程之間共享
即使我們假設單獨的方法是線程安全的
也無法確保結果是意料之中的
看下面對Event Source的接口
它允許安置句柄和對事件的查詢
interface EventSource{
Handler getHandler(Event event);
void installHandler(Event event
Handler newHandler);
}
線程之間的交叉調用可能會引起意想不到的結果
假設source域引用一個線程共享的對象
對象很可能在
之間被另一個線程安裝了一個新的句柄
class EventSourceExample{
public void example(Event event
Handler newHandler){
oldHandler = eventSource
getHandler(event); //
//對象很可能在這裡被另一個線程安裝了一個新的句柄
eventSource
installHandler(event
newHandler); //
}
private EventSource eventSource;
private Handler oldHandler;
}
為了解決問題
也需要由類的使用者而不是類的設計者來完成
class EventSourceExample{
public void example(Event event
Handler newHandler){
synchronized(eventSource){
oldHandler = eventSource
getHandler(event);
eventSource
installHandler(event
newHandler);
}
}
private EventSource eventSource;
private Handler oldHandler;
}
我們假設
目標對象eventSource是遠程的
執行每一個方法體的時間和通訊的延遲相比是很短的
在這個例子中
eventSource的方法被調用了兩次
並可能在其他的實例中重復多次
因而
開銷也是至少兩倍
此外還有一個問題是對外部的synchronized同步塊的使用需求
對synchronized塊的使用之所以會失敗
主要因為我們通過代理對象來完成工作
所以
調用者的synchronized塊
同步的是代理對象而不是最終的目標對象
調用者不可能對其行為做太多的保證
Combined Method必須在分布的環境
或者
線程環境中同時執行
它反映了用戶直接的應用
恢復策略和一些笨拙的方法被封裝到Combined Method中
並簡化了接口
減少了接口中不需要的累贅
Combined Method的效果是支持一種更像事務處理風格的設計
在一個組合的Command
Query中提供一個單獨的Query方法通常是合理的
提供分離的Command方法是不太常見的
因為Combined Method可以完成這一工作
只要調用者簡單的忽略返回結果
如果返回一個結果招致一個開銷的話
才可能會提供一個單獨的Command方法
回到前一個例子中
如果installHandler method返回上一次安裝的句柄
則設計變得更加簡單和獨立
interface EventSource{
Handler installHandler(Event event
Handler newHandler);
}
客戶代碼如下
class EventSourceExample{
public void example(Event event
Handler newHandler){
oldHandler = eventSource
installHandler(event
newHandler);
}
private EventSource eventSource;
private Handler oldHandler;
}
這樣
我們給調用者提供了一個更加安全的接口
並且不再需要他們解決線程的問題
從而降低了風險和代碼量
將類設計的職責全部給了類設計者而不是推給用戶
即使有代理對象的出現也不會影響到正確性
一個Combined Method可以是許多Query的集合
許多Command的集合
或者兩者兼有
這樣
它可能補充Command
Query方法
也可能與之相抵觸
當沖突發生的時候
優先選擇Combined Method會產生一個不同的正確性和適用性
在另一個例子中
我們考慮獲得資源的情況
假設
在下面的接口中
方法acquire在資源可用前阻塞
interface Resource{
boolean isAcquired();
void acquire();
void release();
}
類似於下面的代碼會在一個線程系統中推薦使用
class ResourceExample{
public void example(){
bool
From:http://tw.wingwit.com/Article/program/Java/hx/201311/26294.html