目標
規約,前置後置條件,欠定規約、非確定規約、陳述式、操作式規約、規約的強度及其比較
規約
作用
- 給自己和別人寫出設計決策:如final、資料型別定義
- 作為契約,服務端與客戶端達成一致
- 呼叫方法時雙方都要遵守
- 便於定位錯誤
- 解耦,無需告訴客戶端具體實現,變化也無需通知客戶端,扮演防火牆角色
- 判斷行為等價性
內容
- 輸入輸出的資料型別
- 功能和正確性
- 效能
行為等價性
站在客戶端視角看一個行為是否具有等價性。如果兩個函式都符合同一個Spec,則他們等價
Spec的結構
- 前置條件:對客戶端的約束,在使用方法時必須滿足的條件,用
@param
,並用@requires
進行說明 - 後置條件:對開發者的約束,方法結束時必須滿足的條件,用
@return
和@throws
,並用@effects
進行描述 - 契約:前置條件滿足了,則後置條件必須滿足
Spec不需要說明方法內部變數和類的私有方法或變數
😀方法內部儘量不要修改傳入的引數,不要設計mutating的spec,容易引發錯誤。除非必須是mutator方法的spec,否則避免使用mutable的類與方法。
📕因為程式中很有可能有多個變數指向同一個可變物件(別名),在類的實現體或客戶端儲存別名的情況下,可能導致修改併產生bug
同時避免使用可變的全域性變數
🌰例子:
客戶端為了使用者隱私,因此隱藏了id前5位
char[] id = getMitId("bitdiddle");
for (int i = 0; i < 5; ++i) {
id[i] = '*';
}
System.out.println(id);
服務端擔心效率,所以採用了cache全域性可變變數(char[]可變)
private static Map<String, char[]> cache = new HashMap<String, char[]>();
public static char[] getMitId(String username) throws NoSuchUserException {
// see if it's in the cache already
if (cache.containsKey(username)) {
return cache.get(username);
}
// ... look up username in MIT's database ...
// store it in the cache for future lookups
cache.put(username, id);
return id;
}
由於char[]可變,修改前五位會導致Map中的資料也被更改。所以最好採用String
設計規約
對規約分類
透過規約的確定性、陳述性、和強度來判斷“哪個更好”
按強度
前置條件更弱且後置條件更強
也可能無法比較:
- 存在某些實現同時滿足 𝑆1 和 𝑆3,也存在某些實現只滿足 𝑆1 或 𝑆3
- 沒有實現同時滿足兩者
欠定/確定規約
確定:給定一個滿足precondition的輸入,其輸出是唯一的、明確的
欠定:同一個輸入可以有多個合法輸出,通常有確定的實現
非確定:同一個輸入,多次執行時得到的輸出可能不同
操作式/宣告式規約
操作式:例如虛擬碼,用它解釋服務端實現的細節。但最好不要使用它,把實現細節放在實現體內部註釋而不是規約中
宣告式:沒有內部實現的描述,只有輸入得到輸出
🌰例子:
第一個說了傳到一個新的類,但這是具體實現細節
第二個說了遍歷所有元素,這也是具體實現細節
圖例規約
規約限定了範圍,可選擇落在規約中的任意具體實現
更強的規約表示為更小的區域。比如更強的後置條件、更弱的前置條件都意味著實現的自由度更低
設計好的規約
好的方法:並非程式碼好,而是spec的設計,使client用著舒服,開發者編著舒服
1.內聚的
Spec描述的方法應單一、簡單、易理解
分離:規約做了兩件事,所以要分離開形成兩個方法。可以使spec更容易理解,且耦合性低應對變化。如下
public static int LONG_WORD_LENGTH = 5;
public static String longestWord;
/**
* Update longestWord to be the longest element of words, and print
* the number of elements with length > LONG_WORD_LENGTH to the console.
* @param words list to search for long words
*/
public static void countLongWords(List<String> words) {}
2.資訊豐富的
不能引起客戶端的歧義
🌰例子:客戶端不知道返回null是因為原來繫結的值是null,還是因為不存在舊值
static V put(Map<K,V> map, K key, V val)
/**
* requires: `val`可以為`null`,`map`可以包含`null`值
* effects: 將`(key, val)`插入到對映中,如果存在相同的鍵,則覆蓋舊值。返回該鍵的舊值,如果不存在舊值,則返回`null`
*/
3.足夠強
太弱的spec,client不放心、不敢用 (因為沒有給出足夠的承諾)。
開發者應儘可能考慮各種特殊情況,在post-condition給出處理措施
🌰例子:客戶端在得到Exception的時候,不知道哪些元素被新增了,需要自己定位。應該完善exception
static void addAll(List<T> list1, List<T> list2)
effects: adds the elements of list2 to list1,
unless it encounters a null element,
at which point it throws a NullPointerException
4.太強實現難度大
這個沒啥說的
5.使用抽象型別
給方法實現體與客戶端更大的自由度
static ArrayList<T> reverse(ArrayList<T> list)
effects: returns a new list which is the reversal of list, i.e.
newlist[i] == list[n-i-1]
for all 0 <= i < n, where n = list.size()
前置條件/後置條件的一些問題
❓是否檢驗前置條件:通常來說,使用方法檢驗前置條件,成本昂貴
因此,通常選擇使用前置條件,把責任交給client
❓減弱前置條件:客戶端不喜歡太強的前置條件,因此通常是減弱,並用丟擲異常來替代,並且要儘可能在錯誤根源處fail,避免錯誤擴散,難以定位
❓是否使用前置條件:(1) check的代價;(2) 方法的使用範圍
- 如果只在類的內部使用該方法(private),那麼可以不使用前置條件,在使用該方法的各個位置進行check——責任交給內部client;
- 如果在其他地方使用該方法(public),那麼必須要使用前置條件,若client端不滿足則方法丟擲異常。