【軟件構造】第三章第三節 抽象數據型(ADT)

第三章第三節 抽象數據型(ADT)

       3-1節研究了「數據類型」及其特性 ; 3-2節研究了方法和操做的「規約」及其特性;在本節中,咱們將數據和操做複合起來,構成ADT,學習ADT的核心特徵,以及如何設計「好的」ADT。java

Outline

  • ADT及其四種類型
    • ADT的基本概念
    • ADT的四種類型
    • 設計一個好的ADT
  • 表示獨立性
  • 不變量和表示泄露
  • 抽象函數AF和表示不變量RI
    • AF與RI
    • 用註釋寫AF和RI

Notes

## ADT及其四種類型

【ADT的基本概念】git

  • 抽象數據類型(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抽象類型是咱們舉出的例子。下面是規格說明:post

 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 的使用者僅依賴於其公共方法和規格說明,而不依賴其私有的存儲,所以咱們能夠在不檢查和更改全部客戶端代碼的狀況下進行更改。 這就是表示獨立性的力量。  

 

##  不變量(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 永遠不會改變)

  對不可變性的第一個威脅來自使用者能夠直接訪問Tweet內部數據的事實,例如執行以下的引用操做:

1 Tweet t = new Tweet("justinbieber", 
2                     "Thanks to all those beliebers out there inspiring me every day", 
3                     new Date());
4 t.author = "rbmllr";

   這是一個表示泄露(Rep exposure)的簡單例子,這意味着類外的代碼能夠直接修改表示。像這樣的表示暴露不只威脅到不變量,並且威脅到表示獨立性。若是咱們改變類內部數據的1表示方式,使用者也會相應的受到影響。

  幸運的是,java給咱們提供了處理表示暴露的方法:

 1 public class Tweet {
 2     private final String author;
 3     private final String text;
 4     private final Date timestamp;
 5 
 6     public Tweet(String author, String text, Date timestamp) {
 7         this.author = author;
 8         this.text = text;
 9         this.timestamp = timestamp;
10     }
11 
12     /** @return Twitter user who wrote the tweet */
13     public String getAuthor() {
14         return author;
15     }
16 
17     /** @return text of the tweet */
18     public String getText() {
19         return text;
20     }
21 
22     /** @return date/time when the tweet was sent */
23     public Date getTimestamp() {
24         return timestamp;
25     }
26 }

  在private和public關鍵字代表哪些字段和方法可訪問時,只在類內部仍是能夠從類外部訪問。所述final關鍵字還保證該變量的索引不會被更改,對於不可變的類型來講,就是確保了變量的值不可變。

  但這不能解決所有的問題:表示仍然會泄露!考慮這個徹底合理的客戶端代碼,它使用Tweet:

1 /** @return a tweet that retweets t, one hour later*/
2 public static Tweet retweetLater(Tweet t) {
3     Date d = t.getTimestamp();
4     d.setHours(d.getHours()+1);
5     return new Tweet("rbmllr", t.getText(), d);
6 }

  

  retweetLater 但願接受一個Tweet對象而後修改Date後返回一個新的Tweet對象。

  這裏有什麼問題?其中的 getTimestamp 調用返回一個同樣的 Date 對象,它會被t、t.timestamp 和 d 同時索引。所以,當日期對象被突變,d.gsetHours( ) 被調用時,t 也會影響日期,如快照圖所示。

  

  這樣,Tweet的不變性就被破壞,Tweet將本身內部對於可變對象的索引「泄露」了出來,所以整個對象都變成可變的了,使用者在使用時也容易形成隱藏的bug。

  咱們能夠經過使用防護性拷貝來修補這種風險:製做可變對象的副本以免泄漏對錶明的引用。代碼以下:

public Date getTimestamp() {
    return new Date(timestamp.getTime());
}

  可變類型一般具備一個專門用來複制的構造函數,它容許建立一個複製現有實例值的新實例。在這種狀況下,Date的複製構造函數就接受了一個timestamp值,而後產生一個新的對象。

  複製可變對象的另外一種方法是clone(),某些類型但不是所有類型支持該方法。然而clone()在Java中的工做方式存在問題,更多可參考 Effective Java , item 11

   如今咱們已經經過防護性複製解決了 timestamp 返回值的問題。但咱們尚未完成任務!還有表示泄露。考慮這個很是合理的客戶端代碼:

 1 /** @return a list of 24 inspiring tweets, one per hour today */
 2 public static List<Tweet> tweetEveryHourToday () {
 3     List<Tweet> list = new ArrayList<Tweet>(); 
 4     Date date = new Date();
 5     for (int i = 0; i < 24; i++) {
 6         date.setHours(i);
 7         list.add(new Tweet("rbmllr", "keep it up! you can do it", date));
 8     } 
 9     return list;
10 }

 

  此代碼旨在建立24個Tweet對象,爲每一個小時建立一條推文。但請注意,Tweet的構造函數保存傳入的引用,所以全部24個Tweet對象最終都以同一時間結束,如此快照圖所示。

  可是,Tweet的不變性再次被打破了,由於每⼀個Tweet建立時對Date對象的索引都是⼀樣的。因此咱們應該對建立者也進⾏防護性編程:

1 public Tweet(String author, String text, Date timestamp) {
2     this.author = author;
3     this.text = text;
4     this.timestamp = new Date(timestamp.getTime());
5 }

  一般來講,要特別注意ADT操做中的參數和返回值。若是它們之中有可變類型的對象,確保你的代碼沒有直接使⽤索引或者直接返回索引。

  你可能反對說這看起來很浪費。爲何要製做全部這些日期的副本?爲何咱們不能經過像這樣仔細書寫的規範來解決這個問題?

/**
 * Make a Tweet.
 * @param author    Twitter user who wrote the tweet
 * @param text      text of the tweet
 * @param timestamp date/time when the tweet was sent. Caller must never 
 *                   mutate this Date object again!
 */
public Tweet(String author, String text, Date timestamp) {

   這種方法通常只在特不得已的時候使用——例如,當可變對象太大而沒法有效地複製時。可是,由此引起的潛在bug也將不少。除非無可奈何,不然不要把但願寄託於客戶端上,ADT有責任保證本身的不變量,並避免表示泄露。

   最好的辦法就是使用immutable的類型,完全避免表示泄露,例如 java.time.ZonedDateTime 而不是 java.util.Date

 

## 抽象函數AF與表示不變量RI

【AF與RI】

  

  • 在研究抽象類型的時候,先思考一下兩個值域之間的關係:
    • 表示域(rep values)裏面包含的是值具體的實現實體。通常狀況下ADT的表示比較簡單,有些時候須要複雜表示。 
    • 抽象域(A)裏面包含的則是類型設計時支持使用的值。這些值是由表示域「抽象/想象」出來的,也是使用者關注的。
  • ADT實現者關注表示空間R,用戶關注抽象空間A 。
  • R->A的映射特色:
    • 每個抽象值都是由表示值映射而來 ,即滿射:每一個抽象值被映射到一些rep值
    • 一些抽象值是被多個表示值映射而來的,即未必單射:一些抽象值被映射到多個rep值
    • 不是全部的表示值都能映射到抽象域中,即未必雙射:並不是全部的rep值都被映射。

 

  • 抽象函數(AF):R和A之間映射關係的函數
AF : R → A 
  • 表示不變量(RI):將rep值映射到布爾值
RI : R → boolean  
    • 對於表示值r,當且僅當r被AF映射到了A,RI(r)爲真。 
    • 表示不變性RI:某個具體的「表示」是不是「合法的」
    • 也可將RI看做:全部表示值的一個子集,包含了全部合法的表示值
    • 也可將RI看做:一個條件,描述了什麼是「合法」的表示值
    • 在下圖中,綠色表示的就是RI(r)爲真的部分,AF只在這個子集上有定義。

  

  • 表示不變量和抽象函數都應該記錄在代碼中,就在表明自己的聲明旁邊,如下圖爲例 

public class CharSet {
    private String s;
    // Rep invariant:
    //   s contains no repeated characters
    // Abstraction function:
    //   AF(s) = {s[i] | 0 <= i < s.length()}
    ...
}

 

public class CharSet {
    private String s;
    // Rep invariant:
    //   s[0] <= s[1] <= ... <= s[s.length()-1]
    // Abstraction function:
    //   AF(s) = {s[i] | 0 <= i < s.length()}
    ...
}

 

public class CharSet {
    private String s;
    // Rep invariant:
    //   s.length() is even
    //   s[0] <= s[1] <= ... <= s[s.length()-1]
    // Abstraction function:
    //   AF(s) = union of {s[2i],...,s[2i+1]} for 0 <= i < s.length()/2
    ...
}

【用註釋寫AF和RI】

  • 在抽象類型(私有的)表示聲明後寫上對於抽象函數和表示不變量的註解,這是一個好的實踐要求。咱們在上面的例子中也是這麼作的。
  • 在描述抽象函數和表示不變量的時候,注意要清晰明確:
    • 對於RI(表示不變量),僅僅寬泛的說什麼區域是合法的並不夠,你還應該說明是什麼使得它合法/不合法。
    • 對於AF(抽象函數)來講,僅僅寬泛的說抽象域表示了什麼並不夠。抽象函數的做用是規定合法的表示值會如何被解釋到抽象域。做爲一個函數,咱們應該清晰的知道從一個輸入到一個輸入是怎麼對應的。
  • 本門課程還要求你將表示暴露的安全性註釋出來。這種註釋應該說明表示的每一部分,它們爲何不會發生表示暴露,特別是處理的表示的參數輸入和返回部分(這也是表示暴露發生的位置)。
  • 下面是一個Tweet類的例子,它將表示不變量和抽象函數以及表示暴露的安全性註釋了出來:
 1 // Immutable type representing a tweet.
 2 public class Tweet {
 3 
 4     private final String author;
 5     private final String text;
 6     private final Date timestamp;
 7 
 8     // Rep invariant:
 9     //   author is a Twitter username (a nonempty string of letters, digits, underscores)
10     //   text.length <= 140
11     // Abstraction function:
12     //   AF(author, text, timestamp) = a tweet posted by author, with content text, 
13     //                                 at time timestamp 
14     // Safety from rep exposure:
15     //   All fields are private;
16     //   author and text are Strings, so are guaranteed immutable;
17     //   timestamp is a mutable Date, so Tweet() constructor and getTimestamp() 
18     //        make defensive copies to avoid sharing the rep's Date object with clients.
19 
20     // Operations (specs and method bodies omitted to save space)
21     public Tweet(String author, String text, Date timestamp) { ... }
22     public String getAuthor() { ... }
23     public String getText() { ... }
24     public Date getTimestamp() { ... }
25 }

  注意到咱們並無對 timestamp 的表示不變量進行要求(除了以前說過的默認 timestamp!=null)。可是咱們依然須要對timestamp 的表示暴露的安全性進行說明,由於整個類型的不變性依賴於全部的成員變量的不變性。

相關文章
相關標籤/搜索