辉夜的博客

繁花似锦,辉夜如昼

0%

软件构造-关于表示泄露1-基本内容

我们将讨论表示泄露的几种情况,以及如何防止表示泄露
原创性声明:每一个字(除了代码和引用)都是我手敲的。

表示泄露的定义

表示泄露,即Representation Exposure,指的是client端程序可以通过某种手段
查询、访问、修改ADT的内部结构
我们将通过一些例子来感受这个定义。

最基本的表示泄露-来自访问权限

在java的设计中,一个字段的访问权限有四种。
访问权限
为了避免表示泄露,如果一个字段可以是private,那么它就应当是private
下面我们来看一个因为访问权限导致表示泄露的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/**
* 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;
}
}

对于这个结构,我们在创建之后可以任意访问修改它的字段,因为字段被声明为public:

1
2
3
4
Tweet t = new Tweet("justinbieber", 
"Thanks to all those beliebers out there inspiring me every day",
new Date());
t.author = "rbmllr";

你可能会想:为什么改变一个对象的字段被认为是一种“泄露”呢?我们从软件构造的3个角度来回答:

  1. safe from bug: 通过赋值改变一个字段可能会导致错误,例如赋值不匹配的类型或无意义的类型。
  2. esay to understand: 赋值一个字段的行为可能是令人困惑的,尤其是当各个字段的含义并不是那么清楚的时候。
  3. ready for change:一旦对象的内部表示发生变化,所有的赋值都必须手动重写。如果用方法进行赋值,则可以很方便的重构。

此外,随意修改变量使得我们不能构造“immutable”的对象,这将让我们的程序充满了危险,我们不得不非常小心的应对每一次方法调用,
我们不知道返回之后的对象是否保持它原来的样子,这真是太可怕了!(译制腔)

为此,一个常见的方法是将所有字段声明为private, 并且如果要构建immutable对象的话,声明为private final.

稍微间接一点的表示泄露-来自传递引用

当我们把所有字段设置为私有,你可能松了口气:现在我的数据类型是安全的了。但还没结束——客户端总是需要了解ADT的“属性”,除非
这个ADT是一个纯粹的功能类(比如Math)。既然如此,我们必须为客户端服务,把ADT的内部表示转换为客户端感兴趣的属性。大部分情况下,属性和内部表示的对应关系是比较简单的,例如我们为Tweet类型增加获取作者、内容、发送时间的方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/** @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;
}

看上去没什么问题,客户端不能直接访问我们的内部表示,因此如果我们要修改内部表示,例如把text分为title和content,我们只需要重写getText,比如返回title + content,而无需客户端做任何改动。

但是还存在这样的一种情况:客户端拿到了这个属性之后,又修改了这个属性,这会发生什么呢?
考虑下面的代码:

1
2
3
4
5
6
/** @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);
}

客户端获取了发推时间后,把时间加了一个小时,又用这个时间创建了新的Tweet。这里的d是通过getTimestamp()获得的,而在d上调用了
setHours()方法将会改变d的值!因此结果是,t和新创建的Tweet现在都指向了一小时后的时间。
表示泄露-来自传递引用
这个例子中,我们的getter传递了一个引用类型Date,一个可变数据类型。因此,在引用d上调用mutator导致了表示泄露。
我们该怎样修改这个代码?通过一种称之为“防御性拷贝”的方式,创建引用的一个副本。这样对返回值的修改便不会影响到对象本身。具体来讲:

1
2
3
public Date getTimestamp() {
return new Date(timestamp.getTime());
}

现在,我们就避免了getter(observor的一种)造成的访问泄露。对于所有的observor方法(返回值非空),我们都需要做如下的检查:只要返回的值不是基本数据类型,并且是可变数据类型,那么就要进行防御式拷贝。反之,如果返回值是基本数据类型或者是不可边类型,则可以直接返回。

稍微间接一点的表示泄露-来自接收引用

引用类型始终是危险的:ADT内部的引用类型也可能被外部的客户端程序持有,这时一旦外部引用调用了mutator,ADT的内部表示也将同步发生变化。我们来看下面的例子:

1
2
3
4
5
6
7
8
9
10
/** @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;
}

这个例子中,我们试图创建24个Tweet,间隔1小时。然而,我们的构造器方法会直接将时间戳赋值给内部属性,因此当我们在循环中调用setHours时,所有Tweet内部的timestamp都将同步变化。最终,我们得到了24个时间相同的Tweet。

1
2
3
4
5
6
7
8
9
10
11
/**
* 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;
}

表示泄露-接收引用

为此,我们在接收引用时也要使用防御式拷贝。如下所示:

1
2
3
4
5
public Tweet(String author, String text, Date timestamp) {
this.author = author;
this.text = text;
this.timestamp = new Date(timestamp.getTime());
}

同样的,对基本数据类型和不可边数据类型,如String,不需要进行防御式拷贝。

总结

这就是MIT课件中关于表示泄露的内容。