【軟件構造】第三章第三節 抽象數據型(ADT)
第三章第三節 抽象數據型(ADT)
3-1節研究了“數據類型”及其特性 ; 3-2節研究了方法和操作的“規約”及其特性;在本節中,我們將數據和操作復合起來,構成ADT,學習ADT的核心特征,以及如何設計“好的”ADT。
Outline
- ADT及其四種類型
- ADT的基本概念
- ADT的四種類型
- 設計一個好的ADT
- 表示獨立性
- ADT的特性
- 不變量
- 表示泄露
- 抽象函數AF
- 表示不變量RI
- 以註釋的形式撰寫AF、RI
Notes
## ADT及其四種類型
【ADT的基本概念】
- 抽象數據類型(Abstract Data Type,ADT)是是指一個數學模型以及定義在該模型上的一組操作;即包括數據數據元素,數據關系以及相關的操作。
- ADT具有以下幾個能表達抽象思想的詞:
- 抽象化:用更簡單、更高級的思想省略或隱藏低級細節。
- 模塊化: 將系統劃分為組件或模塊,每個組件可以設計,實施,測試,推理和重用,與系統其余部分分開使用。
- 封裝:圍繞模塊構建墻,以便模塊負責自身的內部行為,並且系統其他部分的錯誤不會損壞其完整性。
- 信息隱藏: 從系統其余部分隱藏模塊實現的細節,以便稍後可以更改這些細節,而無需更改系統的其他部分。
- 關註點分離: 一個功能只是單個模塊的責任,而不跨越多個模塊。
- 與傳統類型定義的差別:
- 傳統的類型定義:關註數據的具體表示。
- 抽象類型:強調“作用於數據上的操作”,程序員和client無需關心數據如何具體存儲的,只需設計/使用操作即可。
-
ADT是由操作定義的,與其內部如何實現無關!
【ADT的四種類型】
- 前置定義:mutable and immutable types
- 可變類型的對象:提供了可改變其內部數據的值的操作。Date
- 不變數據類型: 其操作不改變內部值,而是構造新的對象。String
- Creators(構造器):
- 創建某個類型的新對象,?個創建者可能會接受?個對象作為參數,但是這個對象的類型不能是它創建對象對應的類型。可能實現為構造函數或靜態函數。(通常稱為工廠方法)
- t* -> T
- 栗子:Integer.valueOf( )
- Producers(生產器):
- 通過接受同類型的對象創建新的對象。
- T+ , t* -> T
- 栗子:String.concat( )
- Observers(觀察器):
- 獲取抽象類型的對象然後返回一個不同類型的對象/值。
- T+ , t* -> t
- 栗子:List.size( ) ;
- Mutators(變值器):
- 改變對象屬性的方法 ,
- 變值器通常返回void,若為void,則必然意味著它改變了對象的某些內部狀態;當然,也可能返回非空類型
- T+ , t* -> t || T || void
- 栗子:List.add( )
- 解釋:T是ADT本身;t是其他類型;+ 表示這個類型可能出現一次或多次;* 表示可能出現0次或多次。
- 更多栗子:
【設計一個好的ADT】
設計好的ADT,靠“經驗法則”,提供一組操作,設計其行為規約 spec
- 原則 1:設計簡潔、一致的操作。
- 最好有一些簡單的操作,它們可以以強大的方式組合,而不是很多復雜的操作。
- 每個操作應該有明確的目的,並且應該有一致的行為而不是一連串的特殊情況。
- 原則 2:要足以支持用戶對數據所做的所有操作需要,且用操作滿足用戶需要的難度要低。
- 提供get()操作以獲得list內部數據
- 提供size()操作獲取list的長度
- 原則 3:要麽抽象、要麽具體,不要混合 —— 要麽針對抽象設計,要麽針對具體應用的設計。
【測試ADT】
- 測試creators, producers, and mutators:調用observers來觀察這些 operations的結果是否滿足spec;
- 測試observers: 調用creators, producers, and mutators等方法產生或改變對象,來看結果是否正確。
## 表示獨立性
- 表示獨立性:client使用ADT時無需考慮其內部如何實現,ADT內部表示的變化不應影響外部spec和客戶端。
- 除非ADT的操作指明了具體的前置條件/後置條件,否則不能改變ADT的內部表示——spec規定了 client和implementer之間的契約。
【一個例子:字符串的不同表示】
讓我們先來看看一個表示獨立的例子,然後考慮為什麽很有用,下面的MyString抽象類型是我們舉出的例子。下面是規格說明:
1 /** MyString represents an immutable sequence of characters. */ 2 public class MyString { 3 4 //////////////////// Example of a creator operation /////////////// 5 /** @param b a boolean value 6 * @return string representation of b, either "true" or "false" */ 7 public static MyString valueOf(boolean b) { ... } 8 9 //////////////////// Examples of observer operations /////////////// 10 /** @return number of characters in this string */ 11 public int length() { ... } 12 13 /** @param i character position (requires 0 <= i < string length) 14 * @return character at position i */ 15 public char charAt(int i) { ... } 16 17 //////////////////// Example of a producer operation /////////////// 18 /** Get the substring between start (inclusive) and end (exclusive). 19 * @param start starting index 20 * @param end ending index. Requires 0 <= start <= end <= string length. 21 * @return string consisting of charAt(start)...charAt(end-1) */ 22 public MyString substring(int start, int end) { ... } 23 }
使用者只需要/只能知道類型的公共方法和規格說明。下面是如何聲明內部表示的方法,作為類中的一個實例變量:
private char[] a;
使用這種表達方法,我們對操作的實現可能是這樣的:
1 public static MyString valueOf(boolean b) { 2 MyString s = new MyString(); 3 s.a = b ? new char[] { ‘t‘, ‘r‘, ‘u‘, ‘e‘ } 4 : new char[] { ‘f‘, ‘a‘, ‘l‘, ‘s‘, ‘e‘ }; 5 return s; 6 } 7 8 public int length() { 9 return a.length; 10 } 11 12 public char charAt(int i) { 13 return a[i]; 14 } 15 16 public MyString substring(int start, int end) { 17 MyString that = new MyString(); 18 that.a = new char[end - start]; 19 System.arraycopy(this.a, start, that.a, 0, end - start); 20 return that; 21 }
執行下列的代碼
MyString s = MyString.valueOf(true); MyString t = s.substring(1,3);
我們用快照圖展示了在使用者進行 subString 操作後的數據狀態:
這種實現有一個性能上的問題,因為這個數據類型是不可變的,那麽 substring 實際上沒有必要真正去復制子字符串到?個新的數組中。它可以僅僅指向原來的 MyString 字符數組,並且記錄當前的起始位置和終?位置。
為了優化,我們可以將這個類的內部表示改為:
private char[] a; private int start; private int end;
有了這個新的表示,操作現在可以這樣實現:
1 public static MyString valueOf(boolean b) { 2 MyString s = new MyString(); 3 s.a = b ? new char[] { ‘t‘, ‘r‘, ‘u‘, ‘e‘ } 4 : new char[] { ‘f‘, ‘a‘, ‘l‘, ‘s‘, ‘e‘ }; 5 s.start = 0; 6 s.end = s.a.length; 7 return s; 8 } 9 10 public int length() { 11 return end - start; 12 } 13 14 public char charAt(int i) { 15 return a[start + i]; 16 } 17 18 public MyString substring(int start, int end) { 19 MyString that = new MyString(); 20 that.a = this.a; 21 that.start = this.start + start; 22 that.end = this.start + end; 23 return that; 24 }
現在運行上面的調用代碼,可用快照圖重新進行 substring 操作後的數據狀態:
由於 MyString 的使用者僅依賴於其公共方法和規格說明,而不依賴其私有的存儲,因此我們可以在不檢查和更改所有客戶端代碼的情況下進行更改。 這就是表示獨立性的力量。
## ADT的特性:不變量(Invariants)與表示泄露
一個好的抽象數據類型的最重要的屬性是它保持不變量。一旦一個不變類型的對象被創建,它總是代表一個不變的值。當一個ADT能夠確保它內部的不變量恒定不變(不受使用者/外部影響),我們就說這個ADT保護/保留自己的不變量。
【一個栗子:表示泄露】
1 /** 2 * This immutable data type represents a tweet from Twitter. 3 */ 4 public class Tweet { 5 6 public String author; 7 public String text; 8 public Date timestamp; 9 10 /** 11 * Make a Tweet. 12 * @param author Twitter user who wrote the tweet 13 * @param text text of the tweet 14 * @param timestamp date/time when the tweet was sent 15 */ 16 public Tweet(String author, String text, Date timestamp) { 17 this.author = author; 18 this.text = text; 19 this.timestamp = timestamp; 20 } 21 }
我們如何保證這些Tweet對象是不可變的,(即一旦創建了Tweet,其author,message和 date 永遠不會改變)對不可變性的第一個威脅來自客戶實際上必須直接訪問其領域的事實。
【軟件構造】第三章第三節 抽象數據型(ADT)