麻省理工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>

相关文章
相关标签/搜索