不变量(Invariants)
对于一个ADT,在它的整个生命周期中都应保持一个性质是不变的,这个性质便是不变量。如前文提到的类的不变性(Immutability)就是一个不变量,指在整个ADT的生命周期中,这个类的属性值均不应改变
表示暴露(representation exposure)
对于下列一个Tweet类,我们说它是存在表示暴露的,即这个ADT的属性暴露给了用户
/**
* This immutable data type represents a tweet from Twitter.
*/
public class Tweet {
public String author;
public String text;
public Date timestamp;
/**
* 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
*/
public Tweet(String author, String text, Date timestamp) {
this.author = author;
this.text = text;
this.timestamp = timestamp;
}
}
用户可以通过如下代码直接访问并改变(不通过Tweet提供的操作)Tweet的属性值,可以发现public声明是罪魁祸首。
Tweet t = new Tweet("justinbieber",
"Thanks to all those beliebers out there inspiring me every day",
new Date());
t.author = "rbmllr";
所以我们想当然的将public改为private,同时添加getter,如下:
public class Tweet {
private final String author;
private final String text;
private final Date timestamp;
public Tweet(String author, String text, Date timestamp) {
this.author = author;
this.text = text;
this.timestamp = timestamp;
}
/** @return Twitter user who wrote the tweet */
public String getAuthor() {
return author;
}
/** @return text of the tweet */
public String getText() {
return text;
}
/** @return date/time when the tweet was sent */
public Date getTimestamp() {
return timestamp;
}
}
但对下列用户使用的代码来说,它仍然存在表示泄露。由于Date是可变类型,用户通过getTimestamp()得到了Date的引用,进而能直接改变Date(不通过Tweet类的方法)。换句话来说,用户本来是想接受一个Tweet对象,返回一个新的Tweet对象,但却意外的把原来的Tweet对象改变了,这打破了Tweet的不变量:不可改变性(Immutability)。
/** @return a tweet that retweets t, one hour later*/
public static Tweet retweetLater(Tweet t) {
Date d = t.getTimestamp();
d.setHours(d.getHours()+1);
return new Tweet("rbmllr", t.getText(), d);
}
思索片刻以后可以作出修改,getter返回一个新的可变类型Date副本给用户,使用户无法改变原Tweet对象,这种技术叫保护复制(defensive copy)
public Date getTimestamp() {
return new Date(timestamp.getTime());
}
但你也许会在使用这个ADT的某一天发现,Tweet仍然存在表示泄露!可以看到由于Date是可变类型,向Tweet的构造器传入一个Date的引用产生一个Tweet对象后,我们可以通过改变这个Date来改变Tweet类内的Date!一切都是因为Date是可变类型:
/** @return a list of 24 inspiring tweets, one per hour today */
public static List<Tweet> tweetEveryHourToday () {
List<Tweet> list = new ArrayList<Tweet>();
Date date = new Date();
for (int i = 0; i < 24; i++) {
date.setHours(i);
list.add(new Tweet("rbmllr", "keep it up! you can do it", date));
}
return list;
}
所以我们得出了最后的修改版本,拷贝一份传入的Date副本,从此副本来构造Tweet,至此终于完美的解决了问题,设计出了一个较好的ADT
public Tweet(String author, String text, Date timestamp) {
this.author = author;
this.text = text;
this.timestamp = new Date(timestamp.getTime());
}
需要指出的是在某些时候,表示暴露的去除十分复杂或几乎不可能,这个时候在规约中规定用户的行为成为没有方法的方法,比如上述Tweet类,规约可以强行规定用户不能改变传入的Date:
/**
* 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) {
当然Java针对这一现象也提供了一些解决办法,称为不可变包裹(Immutable wrappers),如Collections.unmodifiableList()将List转换为一个新的“List”,而这个新的“List”失去了他的改变器(mutators),如set(), add(), remove()等,实现了从可变类型到不可变类型的转变。而当一个ADT终于去除了表示暴露的问题,我们应在注释中添上表示没有暴露的原因:
// 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.
R空间 & A空间
af&ri的引入是从两个空间的定义开始的
R空间(space of representation values),指一个ADT内部实现所用到的所有数据存储、表示的结构,即这个ADT中的数据本来是什么样子的
A空间(space of abstract values),指一个ADT在用户看来包含了哪些数据的值,即这个ADT中的数据看起来是什么样子的
public class CharSet {
private String s;
...
}
对于这个抽象数据类型CharSet,在某一个时刻他的R空间与A空间可能是这样的,R空间可能有很多String,但在用户看来,它里面只有两个字符串的集合。
可以看到上图的几个特征:
1、A空间中每个元素都有R空间元素与其对应
2、一个A空间中的元素可能有多个R空间元素与其对应
3、不是所有的R空间中的元素都对应到A空间中
抽象函数(Abstraction Functions) & 表示不变性(Rep Invariants)
抽象函数是一个从R空间映射到A空间的映射,如上图所示,它不一定是单射,但一定是满射
AF : R → A
表示不变性是一个从R空间映射到boolean的映射,当R空间的r元素映射到了A空间的某个元素时,RI(r)为true,否则为false
RI : R → boolean
下图绿色为满足RI的元素,红色为不满足RI的元素
AF&RI应该在代码中记录,下图记录对应上图(需要注意的是,AF&RI并不属于规约,由于涉及到内部实现,它不应被用户看到)
public class CharSet {
private String s;
// Rep invariant:
// s contains no repeated characters
// Abstraction function:
// AF(s) = {s[i] | 0 <= i < s.length()}
...
}
检测属性
由上图分析可知,我们应尽量减少RI映射为false的R空间数据,因为这一部分数据是没有任何用处的,甚至还会引起错误,所以在ADT中我们应时常检测属性是否出错(R空间是否出现了红色部分数据),一般称为checkRep。
// Check that the rep invariant is true
// *** Warning: this does nothing unless you turn on assertion checking
// by running Java with -enableassertions (or -ea)
private void checkRep() {
assert denominator > 0;
assert gcd(Math.abs(numerator), denominator) == 1;
}
需要强调的是,这个函数应该在每次ADT的属性被创造或改变后调用检测,且修饰词应为private。别忘记前文提到的,我们应尽量避免null的出现,所以不妨放在checkRep里
有益的改变
通过AF&RI,我们可以重新认识不可变类型,我们只需要保证A空间是不可变的,但是R空间在某些时候可以通过一些改变简化我们的操作,而这些改变对于用户来说也是完全不可见的。
总结
1、对于一个ADT的不变量,任何对象的整个生命周期都应保持不变
2、一个好的ADT的不变量由构造器、产生器构建,由观察器、改变器遵守
3、表示不变性声明了ADT属性的合法值,通过checkRep检查
4、抽象函数是一个从具体类型空间到抽象类型空间的映射
5、表示暴露将影响表示不变性与表示独立性