HITSC_5_Designing Specification

Ch1ldKing發表於2024-05-27
目標

規約,前置後置條件,欠定規約、非確定規約、陳述式、操作式規約、規約的強度及其比較

規約

作用

  1. 給自己和別人寫出設計決策:如final、資料型別定義
  2. 作為契約,服務端與客戶端達成一致
  3. 呼叫方法時雙方都要遵守
  4. 便於定位錯誤
  5. 解耦,無需告訴客戶端具體實現,變化也無需通知客戶端,扮演防火牆角色
  6. 判斷行為等價性

內容

  1. 輸入輸出的資料型別
  2. 功能和正確性
  3. 效能

行為等價性

站在客戶端視角看一個行為是否具有等價性。如果兩個函式都符合同一個Spec,則他們等價

Spec的結構

  1. 前置條件:對客戶端的約束,在使用方法時必須滿足的條件,用@param,並用@requires進行說明
  2. 後置條件:對開發者的約束,方法結束時必須滿足的條件,用@return@throws,並用@effects進行描述
  3. 契約:前置條件滿足了,則後置條件必須滿足
    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的輸入,其輸出是唯一的、明確的
欠定:同一個輸入可以有多個合法輸出,通常有確定的實現
非確定:同一個輸入,多次執行時得到的輸出可能不同

操作式/宣告式規約

操作式:例如虛擬碼,用它解釋服務端實現的細節。但最好不要使用它,把實現細節放在實現體內部註釋而不是規約中
宣告式:沒有內部實現的描述,只有輸入得到輸出
🌰例子:image.png
第一個說了傳到一個新的類,但這是具體實現細節
第二個說了遍歷所有元素,這也是具體實現細節

圖例規約

規約限定了範圍,可選擇落在規約中的任意具體實現image.png
更強的規約表示為更小的區域。比如更強的後置條件、更弱的前置條件都意味著實現的自由度更低image.png

設計好的規約

好的方法:並非程式碼好,而是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端不滿足則方法丟擲異常。

相關文章