文章目录
- Software Construction of HIT💯
- Chapter 5 Designing Specification
- Chapter 6 Abstract Data Type (ADT)
- 1 Abstraction and User-Defined Types
- 2 Classifying Types and Operations
- 3 Abstract Data Type Examples
- 4 Designing an Abstract Type
- 5 Representation Independence ❗❗❗
- 6 Testing an Abstract Data Type
- 7 Invariants
- 8 Rep Invariant and Abstraction Function ❗❗❗
- 9 Beneficent mutation
- 10 Documenting the AF, RI, and Safety from Rep Exposure
- 11 How to establish invariants
- 12 ADT invariants replace preconditions
Software Construction of HIT💯
Chapter 5 Designing Specification
1 Functions & methods in programming languages
-
“方法”是程序的“积木”,可以被独立开发、测试、复用。
-
使用“方法”的客户端,无需了解方法内部具体如何工作——“抽象”。
2 Specification: Programming for communication
2.1 Documenting in programming
- Programs have to be written with two goals in mind:
- Communicating with the computer. 代码中蕴含的“设计决策”:给编译器读。
- Communicating with other people. 注释形式的“设计决策”:给自己和别人读。
2.2 Specification and Contract (of a method)
Spec给“供需双方”都确定了责任,在调用的时候双方都要遵守。
只讲“能做什么”,不讲 “怎么实现”。
2.3 Behavioral equivalence
站在客户端视角(客户需求)看行为等价性,根据规约判断是否行为等价。
2.4 Specification structure: pre-condition and post-condition
A specification of a method consists of several clauses:
-
Precondition requires
前置条件:对客户端的约束,在使用方法时必须满足的条件——输入。
-
Postcondition effects
后置条件:对开发者的约束,方法结束时必须满足的条件——输出。
-
Exceptional behavior: what it does if precondition violated
契约:
- 如果前置条件满足了,后置条件必须满足。
- 前置条件不满足,则方法可做任何事情。
Put the preconditions into @param
where possible, and postconditions into @return
and @throws
.
2.5 Specifications for mutating methods😥
程序可能会对输入参数作改变,必须在规约中声明。
- 除非在后置条件里声明过,否则方法内部不应该改变输入参数。
- 应尽量遵循此规则,尽量不设计 mutating的spec,否则就容易引发bugs。
- 程序员之间应达成的默契:除非spec必须如此,否则不应修改输入参数。
2.6 Testing and verifying specifications
黑盒测试完全根据规约。
3 Designing specifications
3.1 Classifying specifications
越强的规约,意味着implementor的自由度和责任越重(后置条件变强了),而client的责任越轻(前置条件变弱了)。
也意味着更少的实现方法能够满足它,更多的客户端能够使用它。
3.2 Diagramming specifications
程序员可以在 规约的范围内自由选择实现方式,客户端无需了解具体使用了哪个实现。
更强的规约,表达为更小的区域(实现的自由度降低)。
3.3 Designing good specifications
Spec描述的功能应单一、简单、易理解:一个规约应该只做一件事
Spec要足够强:应尽可能考虑各种特殊情况,在post-condition给出处理措施
是否使用前置条件取决于(1) check的代价;(2) 方法的使用范围
- 如果只在类的内部使用该方法(private),那么可以不使用前置条件,在使用该方法的各个位置进行check,责任交给内部client;
- 如果在其他地方使用该方法(public),那么必须要使用前置条件,若client端不满足则方法抛出异常
Chapter 6 Abstract Data Type (ADT)
!!!这一章是考试的重中之重!!!
1 Abstraction and User-Defined Types
ADT是由操作定义的,与其内部如何实现无关!
2 Classifying Types and Operations
Mutable and immutable types
-
Creator
t* → T:创建一个该类型的新对象
**可能实现为构造函数,也可能是静态函数(工厂方法)**比如:Arrays.asList()
和List.of()
,以Lab2中的为例:public static <L> Graph<L> empty() { return new ConcreteEdgesGraph<L>(); }
-
Producer
T+, t* → T:接受同类型的对象创建新的对象,如
String
的concat
方法 -
Observer
T+, t*→ t:接受一个同类型对象返回一个不同类型的对象、值,如
List
的size
方法 -
Mutator(immutable类型是没有mutator的)
T+, t* → void | t | T:改变对象的内容,如
List
的add
方法。注意变值器也可能返回非空类型,比如Set.add()
返回的是boolean。
注意非静态方法(构造函数除外)的输入参数还会隐含一个当前对象!
3 Abstract Data Type Examples
List
- creators:
ArrayList
LinkedList
constructors,Collections.singletonList
- producers:
Collections.unmodifiableList
- observers:
size
,get
- mutators:
add
,remove
,addAll
,Collections.sort
String
- creators: String constructors
- producers:
concat
,substring
, toUpperCase - observers:
length
,charAt
- mutators: none
4 Designing an Abstract Type
- 设计简洁、一致的操作:每个操作有一个被明确定义的目的,对不同的数据结构有一致的行为(例如不能为List添加sum方法)
- 要足以支持client对数据所做的所有操作需要,且用操作满足client需要的难度要低
5 Representation Independence ❗❗❗
表示独立性:client使用ADT时无需考虑其内部如何实现,ADT内部表示的变化不应影响外部spec和客户端。
在下面的Family类的例子中:
/**
* 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;
}
}
客户端这样使用,当然是没有问题的,但是如果有一天我们突然想用Set
来实现Family,那么下面的客户端程序就会出现问题,这时候它依赖于Family的内部实现,这显然是破坏了表示独立性(其实内部属性people本来也不应该暴露给客户端)。
void client1(Family f) {
// get youngest person in the family
Person baby = f.people.get(f.people.size()-1);
...
}
因此我们需要修改getMembers方法如下,这样客户端知道返回的是一个List
,他自然就知道怎么访问了。
public List<Person> getMembers() {
return new ArrayList<>(people);
}
总结抽象数据类型在Java中的实现:
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() |
Static 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 |
6 Testing an Abstract Data Type
当我们测试一个抽象数据类型的时候,我们分别测试它的各个操作。
而这些测试不可避免的要互相交互:①只能通过观察者来判断其他的操作的测试是否成功;②而测试观察者的唯一方法是创建对象然后使用观察者。
7 Invariants
保持不变性和避免表示泄漏,是ADT最重要的一个Invariant!
private
表示这个区域只能由同类进行访问; final
确保了该变量的索引不会被更改,对于不可变的类型来说,就是确保了变量的值不可变。但是要说明的是,类似private final Date timestamp;
这样的语句只能保证客户端不能直接索引到你,但是不排除有“内鬼”出卖你,就像下面这样,因为Date
是mutable的,因此这样会导致表示泄露。
private final Date timestamp;
/** @return date/time when the tweet was sent */
public Date getTimestamp() { // 内鬼
return timestamp;
}
通常来说,要特别注意ADT操作中的参数和返回值。如果它们之中有可变类型的对象,确保代码没有直接使用索引(在构造器的地方对于传入的mutable对象)或者直接返回索引(返回自己类里面的mutable对象),这两种情况都应该采用防御式赋值的方法处理。
public Tweet(String author, String text, Date timestamp) {
this.author = author;
this.text = text;
this.timestamp = new Date(timestamp.getTime()); // here
}
public Date getTimestamp() {
return new Date(timestamp.getTime()); // here
}
8 Rep Invariant and Abstraction Function ❗❗❗
抽象函数AF : R → A:R和A之间映射关系的函数,即如何去解释R中的每一个值为A中的每一个值。这种映射是满射但是不一定是单射。
表示不变量RI : R → boolean:RI是R的子集,表示的是能够表示A的值的集合,也就是所谓的合法表示值。
一个ADT的实现不仅是选择表示域R(规格说明)和抽象域A(具体实现),同时也要决定哪一些表示值是合法的(RI表示不变量),合法表示会被怎么解释/映射(AF抽象函数)。
同样的R、同样的RI,也可能有不同的AF,即“不同的解释”;同样的AF、同样的R,也可能有不同的RI。
开发者需要了解:A, AF, creators, observers, rep, RI
客户端只需要了解:A, creators, observers
应该在每一个创建或者改变表示数据的操作后调用checkRep()检查不变量,也就是在使用创建者、生产者以及改造者之后。在RatNum例子中,在两个创建者的最后都使用了 checkRep()进行检查。Observer方法可以不用,但
建议也要检查,以防止你的“万一“。
当在实现检查表示不变量的checkRep()
时,应该显式的检查s!= null
,确保当s
是null
的时候会快速失败。通常来说,这种检查会是自动的,因为很多操作在内容是null
时会自动抛出异常,例如对s.length()
的调用会在s
是null
的时候自动失败报错。但是如果没有对null
的自动检查,就需要显式的使用 assert s!= null
了。
private void checkRep() {
assert s.length() % 2 == 0;
...
}
9 Beneficent mutation
之前对于不可变类型的定义:对象一旦被创建其值不会发生更改。
现在学习了抽象数据类型中的表示域和抽象域,我们可以将这个定义更加细化一下:对象一旦被创建,其抽象值不会发生改变。也就是说,对于使用者来说,其代表的值是不会变的,但是实现者可以在底层对表示域做一些改动,这些不会影响到抽象域的改动就称为友善改动。
举之前提到的RatNum的例子,将表示不变量的限制放宽了,不再要求分子和分母必须是最简形式:
/**
* @return a string representation of this rational number
*/
@Override
public String toString() {
int g = gcd(numerator, denominator);
numerator /= g;
denominator /= g;
if (denominator < 0) {
numerator = -numerator; // !!!
denominator = -denominator; // !!!
}
checkRep();
return (denominator > 1) ? (numerator + "/" + denominator) : (numerator + "");
}
这种mutation只是改变了R值,并未改变A值,对client来说是 immutable的,“AF并非单射”,从一个R值变成了另一个R值。但这并不代表在immutable的类中就可以随意出现mutator!
10 Documenting the AF, RI, and Safety from Rep Exposure
-
RI: 仅仅宽泛的说什么区域是合法的并不够,还应该说明是什么使得它合法/不合法。
-
AF: 仅仅宽泛的说抽象域表示了什么并不够,抽象函数的作用是规定合法的表示值会如何被解释到抽象域。
-
表示暴露的安全性: 这种注释应该说明表示的每一部分,它们为什么不会发生表示暴露,特别是处理的表示的参数输入和返回部分(这也是表示暴露发生的位置)。
Example 1 // Rep invariant: // author is a Twitter username (a nonempty string of letters, digits, underscores) // text.length <= 140 // Abstraction function: // AF(author, text, timestamp) = a tweet posted by author, with content text, // at time timestamp // Safety from rep exposure: // All fields are private; // author and text are Strings, so are guaranteed immutable; // timestamp is a mutable Date, so Tweet() constructor and getTimestamp() // make defensive copies to avoid sharing the rep's Date object with clients. Example 2 // Rep invariant: // denominator > 0 // numerator/denominator is in reduced form, i.e. gcd(|numerator|,denominator) = 1 // Abstraction function: // AF(numerator, denominator) = numerator/denominator // Safety from rep exposure: // All fields are private, and all types in the rep are immutable. Example 3 // Mutable type representing Twitter users' followers. public class FollowGraph { private final Map<String,Set<String>> followersOf; // Rep invariant: // all Strings in followersOf are Twitter usernames // (i.e., nonempty strings of letters, digits, underscores) // no user follows themselves, i.e. x is not in followersOf.get(x) // Abstraction function: // AF(followersOf) = the follower graph where Twitter user x is followed by // user y if and only if followersOf.get(x).contains(y) // Safety from rep exposure: // All fields are private, and followersof 是一个可变的Map,其包含着可变的Set对象,但是getFollowers()在返回时会对Set进行防御性复制,并且其他的参数和返回值都是不可变类型的String或者void // Operations (specs and method bodies omitted to save space) public FollowGraph() { ... } public void addFollower(String user, String follower) { ... } public void removeFollower(String user, String follower) { ... } public Set<String> getFollowers(String user) { ... } }
规格说明不应该谈论具体的表示/实现细节,例如表示域里面的值。它应该认为表示本身(私有区域)对于使用者是不可见的,就像是方法里面的局部变量对外部不可见。这也是为什么我们在注解表示不变量和抽象函数的时候使用的是注释(“//”)而非典型的Javadoc格式(“/**”)。如果我们使用Javadoc注释的话,内部的实现细节会出现在规格说明中,而这会影响表示独立性以及信息隐藏。
ADT的规约里只能使用client可见的内容来撰写,包括参数、返回值、异常等。如果规约里需要提及“值”,只能使用A空间中的“值”。
11 How to establish invariants
如果一个抽象数据类型的不变量满足:
- 创建者或生产者创建(return之前,用checkRep()检查不变量是否得以保持)
- 改造者和观察者保持(return之前,用checkRep()检查不变量是否得以保持)
- 没有表示暴露
那么这种类型的所有实例的不变量都是成立的。
12 ADT invariants replace preconditions
用ADT不变量取代复杂的Precondition,相当于将复杂的precondition封装到了ADT内部。
/**
* @param set1 is a sorted set of characters with no repeats
* @param set2 is likewise
* @return characters that appear in one set but not the other,
* in sorted order with no repeats
*/
static String exclusiveOr(String set1, String set2)
/** @return characters that appear in one set but not the other */
static SortedSet<Character> exclusiveOr(SortedSet<Character> set1, SortedSet<Character> set2);