软件构造知识梳理:
软件构造的质量目标
外部质量因素
- 正确性
- 健壮性
- 可扩展性
- 可复用性
- 兼容性
- 效率
- 可移植性
- 易用性
- 功能
- 及时性
- ......
内部质量因素
- 代码复杂性
- 可读性
- 可理解性
- 整洁
- 规模
要考虑在不同指标之间折中。
最重要的几个质量因素:
- 正确性、健壮性
- 可扩展性、可复用性
数据类型与类型检验
Java中的数据类型
基本数据类型
- int
- long
- double
- char
对象数据类型
- String
- BigInteger
按照Java传统,基本数据类型用小写,对象数据类型开头字母大写
基本 | 对象 |
---|---|
只有值,没有ID | 既有ID,也有值 |
不可变 | 有的可变有的不可变 |
在栈中分配内存 | 在堆中分配内存(局部变量在栈) |
代价低 | 代价高 |
类型检验
- 静态类型语言(比如Java)在编译时进行类型检查
- 动态类型语言,如Python,在运行阶段进行类型检查
静态检查:运行前提示bug
动态检查:运行时提示bug
无检查
静态>动态>无检查
静态检查:关于“类型”的检查,不考虑值
动态检查:关于”值“的检查
可变/不可变
- 改变一个变量:将该变量指向另一个值得存储空间
- 改变一个变量的值:将当前变量指向的存储空间写入一个新的值
尽可能避免变化,以避免副作用
不变性
不变数据类型:一旦被创建,其值不能改变
如果是引用类型,也可以是不变的:一旦确定其指向的对象,不能再被改变指向其他对象
用关键字final声明不可变变量
尽量使用 final变量作为方法的输入参数、作为局部变量
Note:
- final类无法派生子类
- final变量无法改变值/引用
- final方法无法被子类重写
可变对象:拥有方法可以修改自己的值/引用
String 是不可变类型
StringBuilder是可变类型
有区别吗?
- 当只有一个引用指向该值,没有区别
- 有多个引用的时候,差异就出现了
前者s的值仍为”ab“,后者sb的值变为了”abc“
可变类型的优点
- 不可变类型频繁修改会产生大量的临时拷贝(需要垃圾回收)
- 可变类型最少化拷贝以提高效率
使用可变类型可以获得更好的性能,也适合在多个模块之间共享数据
不可变类型的优点
更安全、易于理解、more ready for change
Risk:传递可变对象是一个潜在的错误源泉,一旦被无意中改变很难发现
安全地使用可变类型:局部变量,不会涉及共享;只有一个引用
Array and Collection
- Array是定长数组
- List是变长数组
Collections:
- List
- Set
- Map
Iterator
迭代器是一个对象,它遍历一组元素并逐个返回元素
在集合中删除元素时要使用迭代器
Useful immutable types
-
基本类型及其封装对象类型都是不可变的
-
Java的Collections有提供不可变List、Set、Map的方法
- Collections.unmodifiableList
- Collections.unmodifiableSet
- Collections.unmodifiableMap
这种包装器得到的结果是不可变的,只能看
设计规约
Function/method in programming language
Specification: Programming for communication
Document
- 代码中变量的数据类型定义
- final关键字定义了设计决策
代码中的设计决策:面向编译器
注释形式的“设计决策”:面向用户和程序员
Specification and Contract
- Spec给供需双方都确定了责任,在调用的时候双方都要遵守
- 区分责任
- 客户端无需阅读调用函数的代码,只需理解spec即可
Behavioral equivalence (行为等价性)
两个函数是否可以相互替换
- 根据规约判断行为是否等价
Spec的结构:前置条件和后置条件
- 前置条件:对客户端的约束,在使用方法时必须满足的条件
- 后置条件:对开发者的约束,方法结束时必须满足的条件
- 契约:如果前置条件满足了,后置条件必须满足
静态类型声明是一种规约,可据此进行静态类型检查
方法前的注释也是一种规约,但需人工判定其是否满足
Designing specifications
规约的分类
规约的强度:
S2>S1意味着:
- S2的前置条件更弱
- S2的后置条件更强
就可以用S2替代S1
越强的规约意味着实现者的责任越重,而客户端的责任越轻
- 确定的规约:给第一个满足前置条件的输入,其输出是唯一的、明确的
- 欠定的规约:同一个输入可以有多个输出
- 非确定的规约:同一个输入,多次执行时得到的输出可能不同
操作式规约,例如伪代码
声明式规约:没有内部实现的描述,只有“初-终”状态
声明式规约更有价值
规约的图示
某个具体实现, 若满足规约,则落在其范围内;否则,在其之外
程序员可以在 规约的范围内自由选择实现方式
客户端无需 了解具体使用了哪个实现
更强的规约,表达为更小的区域
设计好的规约
-
Spec描述的功能应该单一、简单、易理解
-
不能让客户端产生理解的歧义
-
足够强(让client放心)
-
足够弱(减轻开发者负担)
-
使用抽象类型
-
precondition or postcondition?
客户端不喜欢太强的precondition,惯用做法是不限定太强的precondition,而是在postcondition中抛出异常:输入不合法
归纳:是否使用前置条件取决于:
- check的代价
- 方法的使用范围
如果只在类内部使用该方法(private),那么可以不用前置条件,在使用该方法的各个位置进行check--责任交给内部client
如果在其他地方使用该方法(public),那么必须使用前置条件,若client端不满足则方法抛出异常。
抽象数据类型
- creator:从无到有,产生一个新的对象
- producer:从旧到新,从旧对象产生新对象
- observer:观察器
- mutator:变值器,改变对象属性,通常返回void,也可能返回非空类型
AF、RI、rep exposure
表示值空间:R,实现者看到和使用的值
抽象值空间:A,client看到和使用的值
R->A:
- 满射
- 未必单射
- 未必双射
AF,RI,safe from rep exposure的docs
- ADT的规约里只能用client可见的内容(A中的值)来撰写,包括参数、返回值、异常等
- 规约里不应谈及任何内部表示(以及R中的值)
用ADT不变量取代复杂的Precondition。