【软件构造】第三章

不变量

好的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。

原文地址:https://www.cnblogs.com/anzhaochong/p/10852687.html

时间: 2024-08-30 10:43:23

【软件构造】第三章的相关文章

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

软件构造 第三章第三节 抽象数据型(ADT) Creators(构造器): 创建某个类型的新对象,?个创建者可能会接受?个对象作为参数,但是这个对象的类型不能是它创建对象对应的类型.可能实现为构造函数或静态函数.(通常称为工厂方法) t* ->  T 例子:Integer.valueOf( ) Producers(生产器): 通过接受同类型的对象创建新的对象. T+ , t* -> T 例子:String.concat( ) Observers(观察器): 获取抽象类型的对象然后返回一个不同类

软件构造 第三章第二节 软件规约

第三章第二节 软件spec 客户端无需阅读调用函数的代码,只需理解spec即可. 精确的规约,有助于区分责任,给"供需双方"确定了责任,在调用的时候双方都要遵守. @param @return @throws 例子: Behavioral equivalence (行为等价性) 根据规约判断是否行为等价 与实现无关! 如果两个函数符合这个规约,故它们等价. Specification Structure 前置条件(precondition):对客户端的约束,在使用方法时必须满足的条件.

软件构造 第三章第五节 ADT和OOP中的等价性

第三章第五节 ADT和OOP中的等价性 1.==与equals ==是引用等价性 :而equals()是对象等价性. == 比较的是索引.更准确的说,它测试的是指向相等(referential equality).如果两个索引指向同一块存储区域,那它们就是==的.对于我们之前提到过的快照图来说,==就意味着它们的箭头指向同一个对象. equals()操作比较的是对象的内容,换句话说,它测试的是对象值相等(object equality).在每一个ADT中,equals操作必须合理定义 2.等价性

软件构造第三章 第五部分

ADT和OOP中的等价性 equal和== hashcode()和equals()方法总是一起被重写 “==”:引用等价性,指向相同的内存地址, equals():对象等价性,在自己定义的ADT时,需要重写Object的equals()/ "=="是对基本数据类型,而对于对象类型,使用equals(). equal的自反.传递.对称 等价的三种定义 1)若AF映射到同样的结果,则等价 2)若两个对象之间满足自反,传递.对称的关系,那么为等价关系 3)站在外部观察者角度发现二者没有区别(

【软件构造】第二章第二节 软件构造的过程、系统和工具

第二章第二节 软件构造的过程.系统和工具 Outline 广义的软件构造过程 编程 静态代码分析 动态代码分析 调试与测试 重构 狭义的软件构造过程 构造系统:经典BUILD场景 构造系统的组件 构造过程和构造描述 Java编译工具 子目标和结构变体 构造工具 Notes ## 广义的软件构造过程 [编程(Coding)] 开发语言:如Java.C.Python 使用IDE(集成开发工具)的优势(组成) 方便编写代码和管理文件(有代码编辑器,代码重构工具.文件和库(Library)管理工具) 能

当代码遇到数理逻辑——面向对象设计与构造第三章总结

在面向对象课程中的第三章,我尝试了基于JML语言的规格化设计,按照AppRunner中的接口文件实现了Path类和PathContainer, Graph, RailWaySystem迭代类.JML语言是一种规格化语言,完全建立于数理逻辑上,既能够为开发者实现类与方法时提供准确的功能参考,也能够在特定工具支持下充当assert的功能和辅助自动生成测试样例. 本篇博客将从以下几方面对第三章进行总结: JML的基本语法与工具链 基于JmlUnitNg的自动测试方法尝试 三次作业架构 程序Bug分析

【软件构造】第一章 软件构造基础(2)

二.软件构造的质量目标 1. 外部属性(主要):影响用户感受,如外观.速度等 (1)正确性:符合规格范围和计划目标 ·只保证各个层面的正确性(假设调用正确) ·检验与调试 ·防御性编程 ·形式化编程 (2)健壮性:响应规格范围外的异常情况 ·提示错误信息 ·正常退出或降级 (3)可扩展性:提供增加新功能的空间 ·固化需求以规避风险 ·设计简洁.离散化 (4)可复用性:使软件模块能够被其他程序使用 ·模式固化 (5)兼容性:跨平台.跨软件交互 ·使用标准文件格式.数据结构.接口,保持一致性 ·定义

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

第六章第三节 面向可维护的构造技术 基于状态的构造技术 状态模式(State Pattern) 备忘录模式(Memento Pattern) Grammar-based construction 使用grammar判断字符串是否合法,并解析成程序里使用的数据结构 . 正则表达式 通常是递归的数据结构 . terminals 终止节点.叶节点 nonterminal 非终止节点(遵循特定规则,利用操作符.终止节点和其他非终止节点,构造新的字符串) 三个基本语法的操作符: 连接,不是通过一个符号,而

软件构造 第七章第四节 调试

第七章第四节 调试 [bug的常见类型] 数学bug:例如 零除法,算术溢出 逻辑bug:例如 无线循环和无限递归 源头bug:例如 使用了为被定义的变量.资源泄漏,其中有限的系统资源如内存或文件句柄通过重复分配耗尽而不释放.缓冲区溢出,其中程序试图将数据存储在分配存储的末尾. 团队工程bug:例如 评论过时或者评论错误.文件与实际产品的区别 ## 调试的基本过程 Debug是测试的后续步骤:测试发现问题,debug消除问题:当防御式编程和测试都无法挡住bug时,我们就必须进行debug了: D

软件构造 第五章第一节 可复用性的度量、形态和外部观察

第五章第一节  可复用性的度量.形态和外部观察 面向复用编程(programming for reuse):开发出可复用的软件 基于复用编程(programming with reuse):利用已有的可复用软件搭建应用系统 代码复用的类型: 白盒复用:源代码可见,可修改和扩展 含义:复制已有代码到正在开发的系统,进行修改 优点:可订制化程度高 缺点:对其修改增加了软件的复杂度,且需要对其内部充分的了解 黑盒服用:源代码不可见,不能修改 含义:只能通过过API接口来使用,无法修改代码 优点:清晰.