我们将讨论表示泄露的几种情况,以及如何防止表示泄露
原创性声明:每一个字(除了代码和引用)都是我手敲的。
表示泄露的定义
表示泄露,即Representation Exposure,指的是client端程序可以通过某种手段
查询、访问、修改ADT的内部结构。
我们将通过一些例子来感受这个定义。
最基本的表示泄露-来自访问权限
在java的设计中,一个字段的访问权限有四种。
为了避免表示泄露,如果一个字段可以是private,那么它就应当是private。
下面我们来看一个因为访问权限导致表示泄露的例子:
1 | /** |
对于这个结构,我们在创建之后可以任意访问修改它的字段,因为字段被声明为public:
1 | Tweet t = new Tweet("justinbieber", |
你可能会想:为什么改变一个对象的字段被认为是一种“泄露”呢?我们从软件构造的3个角度来回答:
- safe from bug: 通过赋值改变一个字段可能会导致错误,例如赋值不匹配的类型或无意义的类型。
- esay to understand: 赋值一个字段的行为可能是令人困惑的,尤其是当各个字段的含义并不是那么清楚的时候。
- ready for change:一旦对象的内部表示发生变化,所有的赋值都必须手动重写。如果用方法进行赋值,则可以很方便的重构。
此外,随意修改变量使得我们不能构造“immutable”的对象,这将让我们的程序充满了危险,我们不得不非常小心的应对每一次方法调用,
我们不知道返回之后的对象是否保持它原来的样子,这真是太可怕了!(译制腔)
为此,一个常见的方法是将所有字段声明为private, 并且如果要构建immutable对象的话,声明为private final.
稍微间接一点的表示泄露-来自传递引用
当我们把所有字段设置为私有,你可能松了口气:现在我的数据类型是安全的了。但还没结束——客户端总是需要了解ADT的“属性”,除非
这个ADT是一个纯粹的功能类(比如Math)。既然如此,我们必须为客户端服务,把ADT的内部表示转换为客户端感兴趣的属性。大部分情况下,属性和内部表示的对应关系是比较简单的,例如我们为Tweet类型增加获取作者、内容、发送时间的方法:
1 | /** @return Twitter user who wrote the tweet */ |
看上去没什么问题,客户端不能直接访问我们的内部表示,因此如果我们要修改内部表示,例如把text分为title和content,我们只需要重写getText,比如返回title + content,而无需客户端做任何改动。
但是还存在这样的一种情况:客户端拿到了这个属性之后,又修改了这个属性,这会发生什么呢?
考虑下面的代码:
1 | /** @return a tweet that retweets t, one hour later*/ |
客户端获取了发推时间后,把时间加了一个小时,又用这个时间创建了新的Tweet。这里的d
是通过getTimestamp()
获得的,而在d上调用了setHours()
方法将会改变d的值!因此结果是,t和新创建的Tweet现在都指向了一小时后的时间。
这个例子中,我们的getter传递了一个引用类型Date
,一个可变数据类型。因此,在引用d上调用mutator导致了表示泄露。
我们该怎样修改这个代码?通过一种称之为“防御性拷贝”的方式,创建引用的一个副本。这样对返回值的修改便不会影响到对象本身。具体来讲:
1 | public Date getTimestamp() { |
现在,我们就避免了getter(observor的一种)造成的访问泄露。对于所有的observor方法(返回值非空),我们都需要做如下的检查:只要返回的值不是基本数据类型,并且是可变数据类型,那么就要进行防御式拷贝。反之,如果返回值是基本数据类型或者是不可边类型,则可以直接返回。
稍微间接一点的表示泄露-来自接收引用
引用类型始终是危险的:ADT内部的引用类型也可能被外部的客户端程序持有,这时一旦外部引用调用了mutator,ADT的内部表示也将同步发生变化。我们来看下面的例子:
1 | /** @return a list of 24 inspiring tweets, one per hour today */ |
这个例子中,我们试图创建24个Tweet,间隔1小时。然而,我们的构造器方法会直接将时间戳赋值给内部属性,因此当我们在循环中调用setHours
时,所有Tweet内部的timestamp都将同步变化。最终,我们得到了24个时间相同的Tweet。
1 | /** |
为此,我们在接收引用时也要使用防御式拷贝。如下所示:
1 | public Tweet(String author, String text, Date timestamp) { |
同样的,对基本数据类型和不可边数据类型,如String,不需要进行防御式拷贝。
总结
这就是MIT课件中关于表示泄露的内容。