Abstraction and User-Defined Types
📕資料抽象:指由一組操作所刻畫的資料型別
傳統的型別關注資料的具體表示,而抽象型別強調“作用於資料上的操作”,無需關注資料如何儲存,而是設計並使用操作
即ADT是由操作定義的,與其內部如何實現無關!
比如,定義一個抽象的Bool類,Bool可以由很多的東西實現,但它的操作決定了它的性質
型別與操作
型別
可變型別和不可變型別
區別:可變型別提供了可改變內部資料的值的操作,而不變型別只能構造新的物件
操作
構造器
初始化物件的狀態,包括底層資料結構等,同時驗證不變性,有以下說法:
- 可以不帶引數,並創造一個空物件
- 可以不帶引數,並創造一個有初始預設值的物件
- 可以帶引數,創造一個初始化物件
變值器
如果返回值為void,則必然意味著它改變了物件的某些內部狀態
也有可能非空
ADT舉例
設計ADT
設計好的ADT,靠“經驗法則”,提供一組操作,設計其行為規約 spec
- 設計簡潔、一致的操作
- 足以支援client對資料所做的所有操作需要,且用操作滿足client需要的難度要低
- 要麼抽象、要麼具體,不要混合---要麼針對抽象設計,要麼針對具體應用的設計
表示獨立性
表示獨立性:client使用ADT時無需考慮其內部如何實現,ADT內部表示的變化不應影響外部spec和客戶端
這意味著當我們對程式碼進行最佳化時,我們無需通知客戶端也無需修改客戶端程式碼,因為最佳化後仍然滿足spec。這就是RI的作用
🌰例子:
違反RI
/**
* Represents a family that lives in a household together.
* A family always has at least one person in it.
* Families are mutable.
*/
class Family {
public List<Person> people;
public List<Person> getMembers() {
return people;
}
}
void client1(Family f) {
Person baby = f.people.get(f.people.size() - 1); // 直接訪問內部表示,違反封裝
// ...
}
問題:直接暴露了people的內部,並且沒有封裝好。查詢方法依賴於具體內部有多少個元素這類的實現細節
改進RI
/**
* Represents a family that lives in a household together.
* A family always has at least one person in it.
* Families are mutable.
*/
class Family {
// 使用Set代替List,以避免重複元素
public Set<Person> people;
/**
* @return a list containing all the members of the family, with no duplicates.
*/
public List<Person> getMembers() {
return new ArrayList<>(people);
}
}
void client3(Family f) {
// 透過getMembers方法獲取成員列表,而不是直接訪問內部表示
Person anybody = f.getMembers().get(0);
// ...
}
- 採用Set,防止成員重複,同時改進了List的實現細節問題
- 透過getmembers來獲取成員列表,而不是直接暴露內部people,防止修改
- getMembers不直接返回people,而是複製一個列表,防止表示暴露
測試一個ADT
- 用observers測試creators、producers、mutators
- 呼叫creators、producers、mutators等產生或修改結果來測試 observers
這麼做有風險,有可能依賴的其他方法有錯誤導致測試結果失效
🌰例子:測試策略:測試觀察者時要呼叫所有的構造器和生產者
//測試構造器
valueOf():
true, false
string = produced by valueOf()
//測試觀察者
length():
string len = 0, 1, n
string = produced by valueOf(), produced by substring()
//測試觀察者
charAt():
string len = 1, n
i = 0, 1, len - 1
string = produced by valueOf(), produced by substring()
//測試生產者
substring():
string len = 0, 1, n
start = 0, middle, len
end = 0, middle, len
end-start = 0, n
string = produced by valueOf(), produced by substring()
在測試時,其實是組合起來測試的,能保證所有方法之間的互動,同時覆蓋全面,如下,每種測試都採用了多種混合的方式
不變數Invariants
一個任何時候都不發生改變的量,用來保持程式的正確性,發現錯誤
防止不變數被修改
不能產生表示洩露
用private 和final 進行修飾來實現一些變數不會被外部獲取或修改
🌰例子:比如這個,當設定timestamp為final時,
d也指向了t的timestamp,則對tweet的timestamp產生了修改,導致表示洩露,修改了不變數,導致Tweet不能保證它是不變數
RI 和 AF
RI
表示不變數RI:某個具體的“表示”是否是“合法的”
也就是R的一個子集,這裡面都是合法的輸入,是一些限定條件
❓為什麼是表示不變數:在方法執行完後,要仍然保持住所設定好的RI
AF
抽象函式:R和A之間對映關係的函式,即如何去解釋R中的每一個值為A中的每一個值
也就是對映到客戶端那裡的值,需要具體解釋是如何對映的
checkRep()
透過一個方法checkRep()
來保證任何時候都是不變數,所有可能改變rep的地方都要檢查