软件构造第三章

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.timestampd 同时索引。所以当我们调用 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, 那么我们只需要添加 privatefinal即可,不用再担心表示暴露。

可变类型的不可变包装

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。

以上是关于软件构造第三章的主要内容,如果未能解决你的问题,请参考以下文章

软件构造课程提纲

软件构造 第七章第三节 断言和防御性编程

软件构造 第三章第三节 抽象数据型(ADT)

软件构造复习

在 Visual Studio 中创建构造函数的代码片段或快捷方式

软件构造第六章第三节 面向可维护的构造技术