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

<font size="3">html

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

因爲咱們學校(哈工大)大二軟件構造課程的大部分素材取自此,也是推薦的閱讀材料之一,因而打算作一些翻譯工做,本身學習的同時也能幫到一些懶得看英文的朋友。另外,該課程的閱讀資料中有許多練習題,可是沒有標準答案,所給出的「正確答案」均爲譯者所寫,有錯誤的地方還請指出。程序員

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

<br />數據庫


<br />編程

譯者:李秋豪 江家偉api

審校:李秋豪數組

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

<br />數據結構

本次課程的目標

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

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

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

<br />

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] 這個非法訪問會被靜態捕捉

<br />

什麼是抽象

抽象數據類型是軟件工程中一個廣泛原則的實例,從它衍生出不少意思相近的名詞。這裏列出了幾個可以表達其中思想的詞:

  • 抽象: 忽略底層的細節而在高層思考
  • **模塊化:**將系統分爲一個模塊,每一個模塊能夠單獨的進行設計、實現、測試、推倒,而且在剩下的開發中進行復用。
  • **封裝:**在模塊的外部創建起一道「圍牆」,使它只對本身內部的行爲負責,而且系統別處的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

<br />

類型和操做的分類

對於類型,無論是內置的仍是用戶定義的,均可以被分爲可改變不可變兩種。其中可改變類型的對象可以被改變:它們提供了改變對象內容的操做,這樣的操做執行後能夠改變其餘對該對象操做的返回值。因此 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

<br />

抽象類型是經過它的操做定義的

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

對於類型T來講,它的操做集合和規格說明徹底定義和構造了它的特性。例如,當咱們談到List類型時,咱們並無特指一個數組或者連接鏈表,而是一系列模糊的值——哪些對象能夠是List類型——知足該類型的規格說明和操做規定,例如 get(), size(), 等等。

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

<br />

設計抽象類型

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

設計少許,簡單,能夠組合實現強大功能的操做而非設計不少複雜的操做。

每一個操做都應該有一個被明肯定義的目的,而且應該設計爲對不一樣的數據結構有一致的行爲,而不是針對某些特殊狀況。例如,或許咱們不該該爲List類型添加一個sum操做。由於這雖然可能對想要操做一個整數列表的用戶有幫助,可是若是用戶想要操做一個字符串列表呢?或者一個嵌套的列表? 全部這些特殊狀況都將會使得sum成爲一個難以理解和使用的操做。

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

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

<br />

表示獨立

特別地,一個好的抽象數據類型應該是表示獨立的。這意味着它的使用和它的內部表示(實際的數據結構和實現)無關,因此內部表示的改變將對外部的代碼沒有影響。例如,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;

--> 具體實現

<br />

抽象數據類型在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(例如一週中有周1、週二等等)來講,是很理想的類型。咱們將會在之後的閱讀中討論枚舉。
  3. 用不變的對象做爲構造者操做。這種模式在不可變類型中很常見,在不可變類型中,最簡單或者空(emptiest譯者:喵喵喵?)的值僅僅是一個屬性爲public的不變量,基於這個不變量,生產者被用來從中構造更復雜的值。

<br />

測試抽象數據類型

當咱們測試一個抽象數據類型的時候,咱們分別測試它的各個操做。而這些測試不可避免的要互相交互:咱們只能經過觀察者來判斷其餘的操做的測試是否成功,而測試觀察者的惟一方法是建立對象而後使用觀察者。

下面是咱們測試 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 操做

<br />

總結

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

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

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

</font>

相關文章
相關標籤/搜索