软件构造第三章
Posted anzhaochong
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了软件构造第三章相关的知识,希望对你有一定的参考价值。
不变量
好的ADT其中最重要的一点就是它会保护/保留自己的不变量。 不变量是一种属性,它在程序运行的时候总是一种状态,而不变性就是其中的一种:一旦一个不变类型的对象被创建,它总是代表一个不变的值。
当一个ADT保护/保留自己的不变量时,对代码的分析会变得更简单。例如,你能够依赖字符串不变性的特点,在分析的时候跳过那些关于字符串的代码;或者当你尝试基于字符串建立其他的不变量的时候,也会变得更简单。与此相对,对于可变的对象,你将不得不对每一处使用它的代码处进行审查。
Tweet t = new Tweet("justinbieber", "Thanks to all those beliebers out there inspiring me every day", new Date()); t.author = "rbmllr";
上例中Tweet中的author属性是public类型,这就是一个表示暴露(Rep exposure)的例子,就是说类外部的代码可以直接修改类内部存储的数据。上面的表示暴露不仅影响到了不变量,也影响到了表示独立性(表示独立性指内部的实现方法与数据结构的变化不影响外部的使用),但这里如果我们改变类内部数据的表示方法,使用者也会受到影响。
public static void main(String[] args) final int value1 = 1; // value1 = 4; final double value2; value2 = 2.0; final Value value3 = new Value(1); value3.v = 4;
我们可以在author属性前改为private final,private
表示这个区域只能由同类进行访问;而final
确保了该变量的索引不会被更改,对于不可变的类型(也就是基本类型)来说,就是确保了变量的值不可变。但是final修饰一个引用变量value3时,这里我们可以看到final修饰引用变量时,只是限定了引用变量的引用不可改变,即不能将value3再次引用另一个Value对象,但是引用的对象的值是可以改变的.由于这个原因上面的ADT中表示还有可能会暴露
/** @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);
其中的 getTimestamp
调用返回一个在t中的Date
对象,它会被 t.timestamp
和 d
同时索引。所以当我们调用 d.setHours()
后,t
也会受到影响。我们可以通过防御性复制来弥补这个问题:在返回的时候复制一个新的对象而不会返回原对象的索引。修改Tweet中的getTimestamp方法如下:
public Date getTimestamp() return new Date(timestamp.getTime());
可变类型通常都有一个专门用来复制的构造者,你可以通过它产生一个一模一样的复制对象。在上面的例子中,Date
的复制构造者就接受了一个timestamp值,然后产生了一个新的对象。另一个复制可变对象的方法是使用clone()
,但是它没有被很多类支持而且在Java中,使用clone()
可能会带来一些麻烦。这里不再赘述。
现在我们已经通过防御性复制解决了 getTimestamp
返回值的问题,但是思考这个使用者的代码:
/** @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;
Tweet的不变性再次被打破了,因为每一个Tweet创建时对Date对象的索引都是一样的。所以我们应该对创建者也进行防御性编程:
public Tweet(String author, String text, Date timestamp) this.author = author; this.text = text; this.timestamp = new Date(timestamp.getTime());
通常来说,你要特别注意ADT操作中的参数和返回值。如果它们之中有可变类型的对象,确保你的代码没有直接使用索引或者直接返回索引。
更好的解决方案是使用不可变类型。例如上面的例子中,如果我们使用的是 java.time.ZonedDateTime
而非 java.util.Date
, 那么我们只需要添加 private
和final
即可,不用再担心表示暴露。
可变类型的不可变包装
Java的collections类提供了一种有趣的“折中”:不可变包装。
Collections.unmodifiableList()
会接收一个(可变)List
然后将其包装为一个不可变对象——它的 set()
, add()
, remove()
,等操作都会抛出异常。所以你可以将一个List
包装为不可变对象,然后将它传入其他地方使用。
这种方法的缺点就是你只能在运行时获得不可变性,而不是编译时。Java不会在编译的时候对你对“不可变”列表的修改提出警告。但是使用不可变的列表、映射、和集合也是减少bug的好方法。
表示不变量和抽象函数
在研究抽象类型的时候,先思考一下两个值域之间的关系:
表示域(space of representation values)里面包含的是值具体的实现实体。在简单的情况下,一个抽象类型只需要实现为单个的对象,但是更常见的情况是使用一个很多对象的网络。
抽象域里面包含的则是类型设计时支持使用的值。这些值是由表示域“抽象/想象”出来的,也是使用者关注的。例如,一个无限整数对象的抽象域是整个整数域,但是它的实现域可能是一个由原始整数类型(有限)组成的数组实现的,而使用者只关注抽象域。
但是,实现者是非常“在意”表示域(和抽象域)的,因为实现者的责任就是实现表示域到抽象域的转换(映射)。
例如,我们选择用字符串来表示一个字符集合:
public class CharSet private String s; ...
这里表示域即为全体字符串,而抽象域是字符集合
如上图所示,表示域R包含的是我们的实现实体(字符串),而抽象域里面是抽象类型表示的字符集合,我们用箭头表示这两个域之间的映射关系。这里要注意几点:
- 每一个抽象值都是由表示值映射而来 。我们之前说过实现抽象类型的意义在于支持对于抽象值的操作,即我们需要能够创建和管理所有的抽象值,因此它们也必须是可表示的。
- 一些抽象值是被多个表示值映射而来的。这是因为表示方法并不是固定的,我们可以灵活的表示一个抽象值。
- 不是所有的表示值都能映射到抽象域中。在上面这个例子中,“abbc”就没有被映射。因为我们已经确定了表示值的字符串中不能含有重复的字符——这样我们的
remove
方法就能在遇到第一个对应字符的时候停止,因为我们知道没有重复的字符。
为了描述这种对应关系和这两个域,我们再定义两个概念:
抽象函数abstraction function是表示值到其对应的抽象值的映射:AF : R → A。快照图中的箭头表示的就是抽象函数,可以看出,这种映射是满射,但不一定是单射(不一定是双射)。
表示不变量rep invariant是表示值到布尔值的映射:RI : R → boolean
对于表示值r,当且仅当r被AF映射到了A,RI(r)为真。换句话说,RI告诉了我们哪些表示值是“良好组织”的(能够去表示A中的抽象值),在下图中,绿色表示的就是RI(r)为真的部分,AF只在这个子集上有定义。
表示不变量和抽象函数都应该在表示声明后注释出来:
public class CharSet private String s; // Rep invariant: // s contains no repeated characters // Abstraction function: // AF(s) = s[i] | 0 <= i < s.length() ...
可以证明,即使有相同的表示域和抽象域,可以有不同的表示不变量以及不同的抽象函数。具体取决于如何对ADT的RI和AF进行设计和解释。
所以,一个ADT的实现不仅是选择表示域(规格说明)和抽象域(具体实现),同时也要决定哪一些表示值是合法的(表示不变量),合法表示会被怎么解释/映射(抽象函数)。
以下哪一些选项是使用者需要了解的?
-
[x] 抽象域
-
[ ] 抽象函数
-
[x] 创建者
-
[x] 观察者
-
[ ] 表示域
-
[ ] 表示不变量
以下哪一些选项是开发者需要了解的?
- [x] 抽象域
- [x] 抽象函数
- [x] 创建者
- [x] 观察者
- [x] 表示域
- [x] 表示不变量
检查表示不变量
表示不变量不仅是一个简洁的数学概念,你还可以通过断言检查它的不变属性来动态捕捉bug。例如RatNum类中(有理数集合)
,这里就举出了一种检查的方法:
// Check that the rep invariant is true // *** Warning: this does nothing unless you turn on assertion checking // by passing -enableassertions to Java private void checkRep() assert denominator > 0; assert gcd(Math.abs(numerator), denominator) == 1;
你应该在每一个创建或者改变表示数据的操作后调用 checkRep()
检查不变量,换句话说,就是在使用创建者、生产者以及改造者之后。
虽然说观察者通常不需要使用 checkRep()
进行检查,但这也是一个不错的主意。因为在每一个操作中调用 checkRep()
检查不变量更能够帮助你捕捉因为表示暴露而带来的bug。
以上是关于软件构造第三章的主要内容,如果未能解决你的问题,请参考以下文章