软件构造复习
1.1 软件构造
中的多维视图
- 按阶段:构造时/运行时视图
- 按动态性:时刻/阶段视图
- 按对象的层次:代码/构件视图
1.2 软件构造的质量目标
外部质量因素
- 正确性
- 健壮性
- 可扩展性
- 可复用性
- 兼容性
- 效率
- 可移植性
- 易用性
- 功能
- 及时性
- Others
内部质量因素
- 代码复杂性
- 可读性
- 可理解性
- 整洁
- 规模
在不同质量因素之间折中很重要
最重要的几个质量因素:
- 正确性、健壮性
- 可扩展性、可复用性
2.1软件生命周期与配置管理
Git之类的,略
2.2 软件构造的过程、系统和工具
略
3.1 数据类型与类型检验
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
这种包装器得到的结果是不可变的,只能看
3.2 设计规约
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
越强的规约意味着实现者的责任越重,而客户端的责任越轻
- 确定的规约:给第一个满足前置条件的输入,其输出是唯一的、明确的
- 欠定的规约:同一个输入可以有多个输出
- 非确定的规约:同一个输入,多次执行时得到的输出可能不同
操作式规约,例如伪代码
声明式规约:没有内部实现的描述,只有“初-终”状态
声明式规约更有价值
规约的图示
某个具体实现, 若满足规约,则落在其范围内;否则,在其之外
程序员可以在 规约的范围内自由选择实现方式
客户端无需 了解具体使用了哪个实现
更强的规约,表达为更小的区域
设计好的规约
-
内聚的(coherent):Spec描述的功能应该单一、简单、易理解
-
信息丰富的:不能让客户端产生理解的歧义
-
足够强(让client放心)
-
足够弱(减轻开发者负担)
-
使用抽象类型
-
precondition or postcondition?
客户端不喜欢太强的precondition,惯用做法是不限定太强的precondition,而是在postcondition中抛出异常:输入不合法
归纳:是否使用前置条件取决于:
- check的代价
- 方法的使用范围
如果只在类内部使用该方法(private),那么可以不用前置条件,在使用该方法的各个位置进行check--责任交给内部client
如果在其他地方使用该方法(public),那么必须使用前置条件,若client端不满足则方法抛出异常。
抽象数据类型
- creator:从无到有,产生一个新的对象
- producer:从旧到新,从旧对象产生新对象
- observer:观察器
- mutator:变值器,改变对象属性,通常返回void,也可能返回非空类型
AF、RI、表示泄露。。。(private,防御性拷贝)
表示值空间:R,实现者看到和使用的值
抽象值空间:A,client看到和使用的值
R->A:
- 满射
- 未必单射
- 未必双射
AF,RI,safe from rep exposure的docs
- ADT的规约里只能用client可见的内容(A中的值)来撰写,包括参数、返回值、异常等
- 规约里不应谈及任何内部表示(以及R中的值)
用ADT不变量取代复杂的Precondition
面向对象的编程
对象、类、属性、方法
- 静态方法不用通过对象调用
- 静态方法无法调用非静态成员(方法和变量)
- 实例方法需要通过对象调用
接口和枚举
- 接口之间可以继承与扩展
- 一个类可以实现多个接口
- 一个接口可以有多个实现类
封装和信息隐藏
信息隐藏(或封装):
-
API同实现分离
-
模块间只通过API通讯
信息隐藏的好处
- 类之间解耦
- 加速系统的开发
- 易于维护
- 进行有效的性能调整
- 提高复用性
接口可以做信息隐藏
- 使用接口类型声明一个变量
- 客户端仅使用接口中定义的方法
- 客户端无法直接访问属性
继承和重写
重写
严格继承:子类只能添加新方法,无法重写超类中的方法
Java中用final声明方法表示不能重写
重写时方法的签名须完全一致
抽象类
- 抽象方法:只有定义没有实现
- 抽象类:至少有一个抽象方法
- 抽象类不能实例化
- 继承某个抽象类的子类实例化时所有父类中的抽象方法必须已经实现
多态、子类型、重载
三种多态
- 特殊多态(ad hoc 多态)
- 参数化多态(泛型)
- 子类型多态(一个变量名可以代表多个类的实例)
特殊多态:一个方法不同的参数列表(Overload,Overload是在编译阶段进行检查,是静态类型检查,与之相反 Override是运行时动态检查)
Override的规则:
- 必须有不同的参数列表(签名不同)
- 可以返回不同的类型
- 可以不同的public/private。。。
- 可以声明不同的异常
- 可以在同一个类内重载,也可以在子类中重载
泛型:PlanningEntry
如果类/接口声明了泛型变量,则称为泛型类/接口
泛型接口可以使非泛型的实现类或泛型的实现类
子类型多态:子类型的规约不能弱化超类型的规约
不同类型的对象可以统一的处理而无需区分
ADT和OOP中的等价性
- 等价关系
- 引用等价性和对象等价性
- 可变数据类型的观察等价性和行为等级等价性
重写equals():参数是Object,如果传入其他参数,则不算重写,而是重载
用@Override声明重写,让编译器确保签名的正确性
重写equals:要先进行类型比较和null值判定
instanceof:动态类型检查
除了用于实现equals方法,尽可能避免使用instanceof和getClass()
等价关系
- 自反、传递、对称
- 等价的对象,其hashCode()的结果必须一致
可变类型
观察等价性:在不改变状态的情况下,两个mutable对象是否看起来一致
行为等价性:调用对象的任何方法都展示出一致的结果
对于可变类型来说,往往倾向于实现严格的观察等价性 但在有些时候,观察等价性可能导致bug,甚至可能破坏RI
JDK中不同的mutable类型的等价性标准不同
- Date:观察等价性
- List:观察等价性
- StringBuilder:行为等价性
对于可变类型,无需重写这equals和hashCode,直接继承Object的两个方法即可。
如果一定要判断两个可变 对象看起来是否一致,最好定义一个新的方法。
可复用性的度量、形态与外部表现
代码级的复用:
- 白盒复用:源代码可见,可修改和扩展
- 黑盒复用:源代码不可见,不能修改
模块级的复用:
类、接口
- 继承
- 委托
库级复用:
API、Package
系统级的复用:
框架:黑盒框架、白盒框架
面向复用的软件构造技术
行为子类型与Liskov替换原则 LSP
如果对于类型T的对象x,q(x) 成立,那么对于类型T的 子类型S的对象y,q(y) 也成立
Java子类型中重写的方法不能抛出额外的异常
LSP:
- 更强的不变量
- 更弱的前置条件
- 更强的后置条件
在编程语言中:
- 前置条件不能强化
- 后置条件不能弱化
- 不变量要保持
- 子类型方法参数:逆变
- 子类型方法的返回值:协变
- 异常类型:协变
协变:子类型中的返回值、异常也变成更具体的类型,例如:
Class T {
Object a(){}
}
Class S extends T {
@Override
String a(){}
}
逆变:参数类型越来越抽象
class T {
void c( String s ) { … }
}
class S extends T {
@Override
void c( Object s ) { … }
}
目前Java遇到这种情况当作Overload对待
Java中的数组是协变的
- 委托发生在对象层面
- 继承发生在类层面
组合
Composite Reuse Principle (CRP) 组合是委派的一种形式
-
Dependency:临时的委派
-
Association:永久的委派
-
Composition:更强的association
-
Aggregation:更弱的association
面向复用的设计模式
适配器模式
- 将某个类/接口转换为client期望的其他形式
装饰器模式
以递归的方式实现
一个接口,定义公共操作
一个起始对象,将通用的方法放入该对象
一个Decorator抽象类,是所有装饰类的基类,具体的装饰类实现具体需要实现的功能
Stack t = new SecureStack( new SynchronizedStack( new UndoStack(s))
装饰器同时使用了继承和委托
Facade 外观模式
提供一个简化的接口供客户端使用
策略模式
有多种不同的算法来实现同一个任务,但需要client根据需要动态切换算法。
模板模式
做事情的步骤一样,但具体方法不同,共性的步骤在抽象类内公共实现,差 异化的步骤在各个子类中实现
使用继承和重写实现模板模式,在框架中用得多