麻省理工18年春軟件構造課程閱讀10“抽象數據類型”


本文內容來自MIT_6.031_sp18: Software Construction課程的Readings部分,采用CC BY-SA 4.0協議。

由於我們學校(哈工大)大二軟件構造課程的大部分素材取自此,也是推薦的閱讀材料之一,於是打算做一些翻譯工作,自己學習的同時也能幫到一些懶得看英文的朋友。另外,該課程的閱讀資料中有許多練習題,但是沒有標准答案,所給出的“正確答案”均為譯者所寫,有錯誤的地方還請指出。

(更新:從第10章開始,只提供正確答案,不再翻譯錯誤答案)




譯者:李秋豪 江家偉

審校:李秋豪

V1.0 Thu Mar 29 00:41:23 CST 2018


本次課程的目標

  • 理解“抽象數據類型(ADT)”
  • 理解“表示獨立”

在這篇閱讀中,我們將會講解一個重要的概念——抽象數據類型,它會幫助我們將數據結構的使用和數據結構的具體實現分開。

抽象數據類型解決了一個很危險的問題:使用者可能對類型的內部表示做假設。我們在后面會探討為什么這種假設是危險的,以及如何避免它。我們也會討論操作符的分類和如何設計好的抽象數據類型。


Java中的訪問控制

閱讀: Controlling Access to Members of a Class

閱讀小練習

閱讀以下代碼並回答問題:

    class Wallet {
        private int amount;

        public void loanTo(Wallet that) {
            // put all of this wallet's money into that wallet
/*A*/       that.amount += this.amount;
/*B*/       amount = 0;
        }

        public static void main(String[] args) {
/*C*/       Wallet w = new Wallet();
/*D*/       w.amount = 100;
/*E*/       w.loanTo(w);
        }
    }

    class Person {
        private Wallet w;

        public int getNetWorth() {
/*F*/       return w.amount;
        }

        public boolean isBroke() {
/*G*/       return Wallet.amount == 0;
        }
    }

假設程序在運行 /*A*/ 語句后立即停止,上圖列出了此時的內部狀態,請問各個數字所標出的方框內應該填上什么?

1 -> w

2 -> that

3 -> loanTo

4 -> 200

Access control A

關於語句 /*A*/,以下哪一個說法是正確的?

that.amount += this.amount;
  • [x] 在Java中允許對this.amount的索引

  • [x] 在Java中允許對 that.amount 的索引

Access control B

關於語句 /*B*/,以下哪一個說法是正確的?

amount = 0;
  • [x] 在Java中允許對 amount 的索引

Access control C

關於語句 /*C*/,以下哪一個說法是正確的?

Wallet w = new Wallet();
  • [x] 在Java中允許對 Wallet() 構造函數的調用

Access control D

關於語句 /*D*/,以下哪一個說法是正確的?

w.amount = 100;
  • [x] 在Java中允許對 w.amount 的訪問

Access control E

關於語句 /*E*/ ,以下哪一個說法是正確的?

w.loanTo(w);
  • [x] 在Java中允許對 loanTo() 的調用
  • [x] 在這句代碼執行之后,w指向的Wallet對象的金額將會是0

Access control F

關於語句 /*F*/,以下哪一個說法是正確的?

return w.amount;
  • [x] 這里關於 w.amount 的索引不會被允許,因為 amount 是在另一個類中的私有區域

  • [x] 這個非法訪問會被靜態捕捉

Access control G

關於語句 /*G*/,以下哪一個說法是正確的?

return Wallet.amount == 0;
  • [x] 這里關於 Wallet.amount 的索引不會被允許,因為 amount 是一個私有地址
  • [x] 這里關於 Wallet.amount 的索引不會被允許,因為 amount 是一個實例變量
  • [x] 這個非法訪問會被靜態捕捉

什么是抽象

抽象數據類型是軟件工程中一個普遍原則的實例,從它衍生出很多意思相近的名詞。這里列出了幾個能夠表達其中思想的詞:

  • 抽象: 忽略底層的細節而在高層思考
  • 模塊化:將系統分為一個模塊,每個模塊可以單獨的進行設計、實現、測試、推倒,並且在剩下的開發中進行復用。
  • 封裝:在模塊的外部建立起一道“圍牆”,使它只對自己內部的行為負責,並且系統別處的bug不會影響到它內部的正確性。
  • 信息隱藏:將模塊的實現細節隱藏,使未來更改模塊內部時不必改變外部代碼。
  • 功能分離:一個模塊僅僅負責一個特性/功能,而不是將一個特性運用在很多模塊上或一個模塊擁有很多特性。

作為一個軟件工程師,你應該知道這些名詞,因為你會在以后的工作中經常遇到它們。這些思想的本質目的都是為了實現我們這門課的三個目標:遠離bug、易於理解、可改動。

事實上,我們在之前的課程中已經碰到過這些思想,特別是在設計方法和規格說明的時候:

  • 抽象:規格說明使得使用者只需要弄懂規格說明並遵守前置條件,而不是讓他們去弄懂底層的代碼實現
  • 模塊化:單元測試和規格說明都幫助了將方法模塊化
  • 封裝:方法中的局部變量都是被封裝的,因為他們僅僅可以在方法內部使用。與此相對的是全局變量和指向可變對象的別名,它們會對封裝帶來很大損害。
  • 信息隱藏:規格說明就是一種信息隱藏,它使得實現者可以自由的更改實現代碼。
  • 功能分離:一個規格說明應該是邏輯明確的,即它不能有很多特性,而應該完成好一個功能。

從今天的課程開始,我們將跳出對方法的抽象,看看對數據的抽象。但是在我們描述數據抽象時方法也會扮演很重要的角色。

用戶定義類型

在早期的編程語言中,用戶只能自己定義方法,而所有的類型都是規定好的(例如整型、布爾型、字符串等等)。而現代編程語言允許用戶自己定義類型對數據進行抽象,這是軟件開發中的一個巨大進步。

對數據進行抽象的核心思想就是類型是通過其對應的操作來區分的:一個整型就是你能對它進行加法和乘法的東西;一個布爾型就是你能對它進行取反的東西;一個字符串就是你能對它進行鏈接或者取子字符串的東西,等等。在一定意義上,用戶在以前的編程語言上似乎已經能夠定義自己的類型了,例如定義一個名叫Date的結構體,里面用int表示天數和年份。但是真正使得抽象類型變得新穎不同的是對操作的強調:用戶不用管這個類型里面的數據是怎么保存表示的,就好像是程序員不用管編譯器是怎么存儲整數一樣。起作用的只是類型對應的操作。

和很多現代語言一樣,在Java中內置類型和用戶定義類型之間的關系很模糊。例如在 java.lang中的類 IntegerBoolean 就是內置的——Java標准中規定它們必須存在,但是它們的定義又是和用戶定義類型的方式一樣的。另外,Java中還保留了原始類型,它們不是類和對象,例如 intboolean ,用戶無法對它們進行繼承。

閱讀小練習

Abstract Data Types

思考抽象數據類型 Bool,它有如下操作:

true : Bool
false : Bool

and : Bool × Bool → Bool
or : Bool × Bool → Bool
not : Bool → Bool

頭兩個操作構建了這個類型對應的兩個值,后三個操作對應邏輯操作 和、或、取非。

以下哪些選項可以是 Bool 具體的實現方法(並且滿足上面的操作符)?

  • [x] 一個比特位,1代表true,0代表false
  • [x] 一個int值,5代表true,8代表false
  • [x] 一個對String對象的索引,"false"代表true, "true" 代表false
  • [x] 一個int值,大於1的質數代表true,其余的代表false

類型和操作的分類

對於類型,不管是內置的還是用戶定義的,都可以被分為可改變不可變兩種。其中可改變類型的對象能夠被改變:它們提供了改變對象內容的操作,這樣的操作執行后可以改變其他對該對象操作的返回值。所以 Date 就是可改變的,因為你可以通過調用setMonth操作改變 getMonth 操作的返回值。但 String 就是不可改變的,因為它的操作符都是創建一個新的 String 對象而不是改變現有的這個。有時候一個類型會提供兩種形式,一種是可改變的一種是不可改變的。例如 StringBuilder就是一種可改變的字符串類型。

而抽象類型的操作符大致分類:

  • 創建者creator:創建一個該類型的新對象。一個創建者可能會接受一個對象作為參數,但是這個對象的類型不能是它創建對象對應的類型。
  • 生產者producer:通過接受同類型的對象創建新的對象。例如, String類里面的 concat 方法就是一個生產者,它接受兩個字符串然后據此產生一個新的字符串。
  • 觀察者observer:接受一個同類型的對象然后返回一個不同類型的對象/值。例如Listsize 方法,它返回一個 int
  • 改造者mutator:改變對象的內容,例如 Listadd 方法,它會在列表中添加一個元素。

我們可以將這種區別用映射來表示:

  • creator : t* → T
  • producer : T+, t* → T
  • observer : T+, t* → t
  • mutator : T+, t* → void | t | T

其中T代表抽象類型本身;t代表其他的類型;+代表這個參數可能出現一次或多次;*代表這個參數可能出現零次或多次。例如, String.concat() 這個接受兩個參數的生產者:

  • concat : String × String → String

有些觀察者不會接受其他類型的參數,例如:

  • size : List → int

而有些則會接受很多參數:

  • regionMatches : String × boolean × int × String × int × int → boolean

構造者通常都是用構造函數實現的,例如 new ArrayList() ,但是有的構造體是靜態方法(類方法),例如 Arrays.asList()String.valueOf ,這樣的靜態方法也稱為工廠方法。

改造者通常沒有返回值(void)。一個沒有返回值的方法一定有副作用 ,因為不然這個方法就沒有任何意義了。但是不是所有的改造者都沒有返回值。例如Set.add() 會返回一個布爾值用來提示這個集合是否被改變了。在Java圖形庫接口中,Component.add() 會將它自己這個對象返回,因此add()可以被連續鏈式調用

抽象數據類型的例子

int 是Java中的原始整數類型,它是不可變類型,沒有改造者。

  • creators: 字面量 0, 1, 2, …
  • producers: 算術符 +, -, *, /
  • observers: 比較符號 ==, !=, <, >
  • mutators: 無

List 是Java中的列表類型,它是可更改類型。另外,List也是一個接口,所以對於它的實現可以有很多類,例如 ArrayListLinkedList.

String 是Java中的字符串類型,它是不可變類型。

  • creators: String 構造函數, valueOf 靜態方法(工廠方法)
  • producers: concat, substring, toUpperCase
  • observers: length, charAt
  • mutators: 無

這個分類告訴了我們一些有用的術語,但它不是完美的。例如對於復雜的數據類型,有些操作可能既是生產者也是改造者。

閱讀小練習

Operations

下面都是我們從Java庫中選取的幾個抽象數據類型的操作,試着通過閱讀文檔將這些操作分類。

提示:注意類型本身是不是參數或者返回值,同時記住實例方法(沒有static關鍵詞的)有一個隱式的參數。

Integer.valueOf()

creator

BigInteger.mod()

producer

List.addAll()

mutator

String.toUpperCase()

producer

Set.contains()

observer

Map.keySet()

observer

BufferedReader.readLine()

mutator


抽象類型是通過它的操作定義的

這一節的重要思想就是抽象類型是通過它的操作定義的.

對於類型T來說,它的操作集合和規格說明完全定義和構造了它的特性。例如,當我們談到List類型時,我們並沒有特指一個數組或者鏈接鏈表,而是一系列模糊的值——哪些對象可以是List類型——滿足該類型的規格說明和操作規定,例如 get(), size(), 等等。

上一段說到的“模糊的值”是指我們不能去檢查數據具體是在類型中怎么存儲的,而是要通過特定的操作去處理。例如上圖中畫出的,通過規格說明這道“防火牆”,我們將類型中具體的實現和這些實現共享的私有數據封裝起來,而用戶只能看到和使用接口上的操作。


設計抽象類型

設計一個抽象類型包括選擇合適的操作以及它們對應的行為,這里列出了幾個重要的規則。

設計少量,簡單,可以組合實現強大功能的操作而非設計很多復雜的操作。

每個操作都應該有一個被明確定義的目的,並且應該設計為對不同的數據結構有一致的行為,而不是針對某些特殊情況。例如,或許我們不應該為List類型添加一個sum操作。因為這雖然可能對想要操作一個整數列表的用戶有幫助,但是如果用戶想要操作一個字符串列表呢?或者一個嵌套的列表? 所有這些特殊情況都將會使得sum成為一個難以理解和使用的操作。

操作集合應該充分地考慮到用戶的需求,也就是說,用戶可以用這個操作集合做他們可能想做的計算。一個較好測試方法是檢查抽象類型的每個屬性是否都能被操作集提取出來。例如,如果沒有get操作,我們就不能提取列表中的元素。抽象類型的基本信息的提取也不應該特別困難。例如,size方法對於List並不是必須的,因為我們可以用get增序遍歷整個列表,直到get執行失敗,但是這既不高效,也不方便。

抽象類型可以是通用的:例如,列表、集合,或者圖。或者它可以是適用於特定領域的:一個街道的地圖,一個員工數據庫,一個電話簿等等。但是一個抽象類型不能兼有上述二者的特性。被設計用來代表一個紙牌序列的Deck類型不應該有一個通用的add方法來向類型實例中添加任意對象,比如整型和字符串類型。反過來說,對於像dealCards這樣的只對特定領域(譯者注:紙牌游戲)有效的方法,把它加入List這樣的通用類型中也是沒有意義的。


表示獨立

特別地,一個好的抽象數據類型應該是表示獨立的。這意味着它的使用和它的內部表示(實際的數據結構和實現)無關,所以內部表示的改變將對外部的代碼沒有影響。例如,List就是表示獨立的——它的使用與它是用數組還是連接鏈表實現無關。

如果一個操作完全在規格說明中定義了前置條件和后置條件,使用者就知道他應該依賴什么,而你也可以安全的對內部實現進行更改(遵循規格說明)。

例子: 字符串的不同表示

讓我們先來看看一個表示獨立的例子,然后想想它為什么很有用。下面的 MyString抽象類型是我們舉出的例子,雖然它遠遠沒有Java中的String操作多,規格說明也有些不同,但是還是有解釋力的。下面是規格說明:

/** MyString represents an immutable sequence of characters. */
public class MyString { 

    //////////////////// Example of a creator operation ///////////////
    /** @param b a boolean value
     *  @return string representation of b, either "true" or "false" */
    public static MyString valueOf(boolean b) { ... }

    //////////////////// Examples of observer operations ///////////////
    /** @return number of characters in this string */
    public int length() { ... }

    /** @param i character position (requires 0 <= i < string length)
     *  @return character at position i */
    public char charAt(int i) { ... }

    //////////////////// Example of a producer operation ///////////////    
    /** Get the substring between start (inclusive) and end (exclusive).
     *  @param start starting index
     *  @param end ending index.  Requires 0 <= start <= end <= string length.
     *  @return string consisting of charAt(start)...charAt(end-1) */
    public MyString substring(int start, int end) { ... }
}

使用者只需要/只能知道這個類型的公共方法和規格說明。

現在讓我們看一個MyString簡單的表示方法,僅僅使用一個字符數組,而且它的大小剛好是字符串的長度,沒有多余的空間:

private char[] a;

如果使用這種表示方法,我們對操作的實現可能就是這樣的:

public static MyString valueOf(boolean b) {
    MyString s = new MyString();
    s.a = b ? new char[] { 't', 'r', 'u', 'e' } 
            : new char[] { 'f', 'a', 'l', 's', 'e' };
    return s;
}

public int length() {
    return a.length;
}

public char charAt(int i) {
    return a[i];
}

public MyString substring(int start, int end) {
    MyString that = new MyString();
    that.a = new char[end - start];
    System.arraycopy(this.a, start, that.a, 0, end - start);
    return that;
}

這里想一個問題:為什么 charAtsubstring 不去檢查參量在合法的范圍內?你認為這種類型的對象對於非法的輸入會有什么反應?

下面的快照圖展示了在使用者進行substring操作后的數據狀態:

MyString s = MyString.valueOf(true);
MyString t = s.substring(1,3);

這種實現有一個性能上的問題,因為這個數據類型是不可變的,那么 substring 實際上沒有必要真正去復制子字符串到一個新的數組中。它可以僅僅指向原來的 MyString 字符數組,並且記錄當前的起始位置和終止位置。

為了實現這種優化,我們可以將內部表示改為:

private char[] a;
private int start;
private int end;

通過這種新的表示方法,我們可以這樣實現操作:

public static MyString valueOf(boolean b) {
    MyString s = new MyString();
    s.a = b ? new char[] { 't', 'r', 'u', 'e' } 
            : new char[] { 'f', 'a', 'l', 's', 'e' };
    s.start = 0;
    s.end = s.a.length;
    return s;
}

public int length() {
    return end - start;
}

public char charAt(int i) {
  return a[start + i];
}

public MyString substring(int start, int end) {
    MyString that = new MyString();
    that.a = this.a;
    that.start = this.start + start;
    that.end = this.start + end;
    return that;
}

現在進行substring操作后的數據狀態:

MyString s = MyString.valueOf(true);
MyString t = s.substring(1,3);

因為 MyString的使用者只使用到了它的公共方法和規格說明(沒有使用私有的存儲表示),我們可以“私底下”完成這種優化而不用擔心影響使用者的代碼。這就是表示獨立的力量。

閱讀小練習

Representation 1

思考下面這個抽象類型:

/**
 * Represents a family that lives in a household together.
 * A family always has at least one person in it.
 * Families are mutable.
 */
class Family {
    // the people in the family, sorted from oldest to youngest, with no duplicates.
    public List<Person> people;

    /**
     * @return a list containing all the members of the family, with no duplicates.
     */
    public List<Person> getMembers() {
        return people;
    }
}

下面是一個使用者的代碼:

void client1(Family f) {
    // get youngest person in the family
    Person baby = f.people.get(f.people.size()-1);
    ...
}

假設所有的代碼都能順利運行( Familyclient1)並通過測試。

現在 Family的數據表示從 List 變為了 Set

/**
 * Represents a family that lives in a household together.
 * A family always has at least one person in it.
 * Families are mutable.
 */
class Family {
    // the people in the family
    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);
    }
}

以下哪一個選項是在 Family 更改后對 client1 的影響?

  • [x] client1 依賴於 Family的數據表示, 並且這種依賴會導致靜態錯誤。

Representation 2

原始版本:

/**
 * Represents a family that lives in a
 * household together. A family always
 * has at least one person in it.
 * Families are mutable. */
class Family {
    // the people in the family,
    // sorted from oldest to youngest,
    // with no duplicates.
    public List<Person> people;

    /** @return a list containing all
     *  the members of the family,
     *  with no duplicates. */
    public List<Person> getMembers() {
        return people;
    }
}

新版本:

/**
 * Represents a family that lives in a
 * household together. A family always
 * has at least one person in it.
 * Families are mutable. */
class Family {
    // the people in the family
    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);
    }
}

使用者 client2的代碼:

void client2(Family f) {
    // get size of the family
    int familySize = f.people.size();
    ...
}

以下哪一個選項是新版本對 client2 的影響?

  • [x] client2 依賴於 Family的表示,這種依賴不會被捕捉錯誤但是會(幸運地)得到正確答案。

Representation 3

原始版本:

/**
 * Represents a family that lives in a
 * household together. A family always
 * has at least one person in it.
 * Families are mutable. */
class Family {
    // the people in the family,
    // sorted from oldest to youngest,
    // with no duplicates.
    public List<Person> people;

    /** @return a list containing all
     *  the members of the family,
     *  with no duplicates. */
    public List<Person> getMembers() {
        return people;
    }
}

新版本:

/**
 * Represents a family that lives in a
 * household together. A family always
 * has at least one person in it.
 * Families are mutable. */
class Family {
    // the people in the family
    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);
    }
}

使用者 client3的代碼:

void client3(Family f) {
    // get any person in the family
    Person anybody = f.getMembers().get(0);
    ...
}

以下哪一個選項是新版本對 client3 的影響?

  • [x] client3 獨立於 Family的數據表示, 所以它依然能正確的工作

Representation 4

對於上面的Family數據類型,對每行/段判斷他是規格說明(specification)還是數據表示(representation)還是具體實現(implementation)?

/**
 * Represents a family that lives in a household together.
 * A family always has at least one person in it.
 * Families are mutable.
 */

--> 規格說明

public class Family {

--> 規格說明

    // the people in the family, sorted from oldest to youngest, with no duplicates.

--> 數據表示

    private List<Person> people;

--> 數據表示

  /**
     * @return a list containing all the members of the family, with no duplicates.
     */

--> 規格說明

 public List<Person> getMembers() {

--> 規格說明

return people;

--> 具體實現


抽象數據類型在Java中的實現

讓我們總結一下我們在這篇文章中討論過的主要思想以及使用JAVA語言特性實現它們的具體方法,這些思想對於使用任何語言編程一般都是適用的。重點在於有很多種方式來實現,很重要的一點是:既要對大概念(比如構造操作:creator operation)有較好的理解,也要理解它們不同的實現方式。

ADT concept Ways to do it in Java Examples
Abstract data type Class String
Interface + class(es) List and ArrayList
Enum DayOfWeek
Creator operation Constructor ArrayList()
Static (factory) method Collections.singletonList(), Arrays.asList()
Constant BigInteger.ZERO
Observer operation Instance method List.get()
Instance method Collections.max()
Producer operation Instance method String.trim()
Static method Collections.unmodifiableList()
Mutator operation Instance method List.add()
Static method Collections.copy()
Representation private fields

這個表中有三項我們還沒有在之前的閱讀中講過:

  1. 使用接口來定義一個抽象數據類型。我們已經看到 ListArrayList 這些例子,並且我們將會在以后的閱讀中討論接口。
  2. 使用枚舉類型(enum)定義一個抽象數據類型。枚舉對於有固定取值集合的ADTs(例如一周中有周一、周二等等)來說,是很理想的類型。我們將會在以后的閱讀中討論枚舉。
  3. 用不變的對象作為構造者操作。這種模式在不可變類型中很常見,在不可變類型中,最簡單或者空(emptiest譯者:喵喵喵?)的值僅僅是一個屬性為public的不變量,基於這個不變量,生產者被用來從中構造更復雜的值。

測試抽象數據類型

當我們測試一個抽象數據類型的時候,我們分別測試它的各個操作。而這些測試不可避免的要互相交互:我們只能通過觀察者來判斷其他的操作的測試是否成功,而測試觀察者的唯一方法是創建對象然后使用觀察者。

下面是我們測試 MyString 類型時對輸入空間的一種可能划分方案:

// testing strategy for each operation of MyString:
//
// valueOf():
//    true, false
// length(): 
//    string len = 0, 1, n
//    string = produced by valueOf(), produced by substring()
// charAt(): 
//    string len = 1, n
//    i = 0, middle, 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()

現在我們試着用測試用例覆蓋每一個分區。注意到 assertEquals 並不能直接應用於 MyString對象,因為我們沒有在 MyString上定義判斷相等的操作,所以我們只能使用之前定義的 valueOf, length, charAt, 以及 substring,例如:

@Test public void testValueOfTrue() {
    MyString s = MyString.valueOf(true);
    assertEquals(4, s.length());
    assertEquals('t', s.charAt(0));
    assertEquals('r', s.charAt(1));
    assertEquals('u', s.charAt(2));
    assertEquals('e', s.charAt(3));
}

@Test public void testValueOfFalse() {
    MyString s = MyString.valueOf(false);
    assertEquals(5, s.length());
    assertEquals('f', s.charAt(0));
    assertEquals('a', s.charAt(1));
    assertEquals('l', s.charAt(2));
    assertEquals('s', s.charAt(3));
    assertEquals('e', s.charAt(4));
}

@Test public void testEndSubstring() {
    MyString s = MyString.valueOf(true).substring(2, 4);
    assertEquals(2, s.length());
    assertEquals('u', s.charAt(0));
    assertEquals('e', s.charAt(1));
}

@Test public void testMiddleSubstring() {
    MyString s = MyString.valueOf(false).substring(1, 2);
    assertEquals(1, s.length());
    assertEquals('a', s.charAt(0));
}

@Test public void testSubstringIsWholeString() {
    MyString s = MyString.valueOf(false).substring(0, 5);
    assertEquals(5, s.length());
    assertEquals('f', s.charAt(0));
    assertEquals('a', s.charAt(1));
    assertEquals('l', s.charAt(2));
    assertEquals('s', s.charAt(3));
    assertEquals('e', s.charAt(4));
}

@Test public void testSubstringOfEmptySubstring() {
    MyString s = MyString.valueOf(false).substring(1, 1).substring(0, 0);
    assertEquals(0, s.length());
}

閱讀小練習

Partition covering

哪一個測試覆蓋了分區“charAt() 以及字符串長度=1”?

  • [x] testMiddleSubstring

哪一個測試覆蓋了分區“子字符串的子字符串”?

  • [x] testSubstringOfEmptySubstring

哪一個測試覆蓋了分區“valueOf(true)”?

  • [x] testValueOfTrue

  • [x] testEndSubstring

Unit testing an ADT

testValueOfTrue測試的是哪一個“單元”?

  • [x] valueOf 操作
  • [x] length 操作
  • [x] charAt 操作

總結

  • 抽象數據類型(ADT)是通過它們對應的操作區分的。
  • 操作可以分類為創建者、生產者、觀察者、改造者。
  • ADT的標識由它的操作集合和規格說明組成。
  • 一個好的ADT應該是簡單,邏輯明確並且表示獨立的。
  • 對於ADT的測試應該對每一個操作進行測試,並同時利用到創建者、生產者、觀察者、改造者。

T將本次閱讀的內容和我們的三個目標聯系起來:

  • 遠離bug. 一個好的ADT會在使用者和實現者之間建立“契約”,使用者知道應該如何使用,而實現者有足夠的自由決定具體實現。
  • 易於理解. 一個好的ADT會將其內部的代碼和信息隱藏起來,而使用者只需要理解它的規格說明和操作即可。
  • 可改動. 表示獨立使得實現者可以在不通知使用者的情況下對ADT內部進行改動。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM