本系列文章
- Chapter 1: Views and Quality Objectives of Software Construction
- Chapter 2: Process and Tools of Software Construction
- Chapter 3: Abstract Data Type and Object-Oriented Programming
- Chapter 4: Reusability-Oriented Software Construction Approaches
- Chapter 5: Maintainability-Oriented Software Construction Approaches
- Chapter 6: Software Construction for Robustness
- Chapter 7: Concurrent and Distributed Programming
Chapter 4: Reusability-Oriented Software Construction Approaches
4.1 Metrics, Morphology and External Observations of Reusability
1. 面向复用编程
-
面向复用编程(programming for reuse):开发可以复用的软件
-
基于复用编程(programming with reuse):复用已有的软件开发
为了降低成本和开发时间,提出了面向复用的编程,所有面向复用的代码都应该经过充分的测试,以保证它的可靠性和稳定性(不能在未来使用的时候发现一堆bug,那就白干了),而因为它是面向复用的,所以在不同的应用里可以保持一致的表现,也就是说对此功能做了标准化。
可复用性的评估
评估的方面:复用的频繁性、复用的代价 (适配)
一个有高可复用性的代码应该有如下特点:小、简单;与标准兼容;灵活可变;可扩展;泛型、参数化;模块化;变化的局部性;稳定;丰富的文档和帮助。
2. 复用的层面
最主要的复用是在代码层面,这也是我们所关注的,但软件构造过程中的任何实体都可能被复用(需求、spec、数据、测试用例、文档等等)
-
源代码层面:方法、语句…
-
模块层面:ADT (类和接口)
-
库层面:API,如.jar文件
-
架构层面:框架
复用分为白盒复用和黑盒复用,白盒复用意味着源码是可见的,对我们来说意义不是很大,更多的是源码不可见的黑盒复用,只有这样才能隔离客户端和ADT的内部实现。
源代码层面的复用
可以在网络上寻找自己需要的代码,但要注意开发商用的软件不能直接复制开源的代码,避免引起法律纠纷。
模块层面的复用
- 通过继承 (Inheritance) 的方式复用父类的代码,同时也可override父类中已存在的方法。
- 另一个复用的方法是 委托(delegation),详见下一小节(4.2)。
库层面的复用
通过导入库来调用库中的API完成复用。
除了导入本地库,也可以通过导入部署在网络上的库来完成复用,如 Web Services / Restful APIs
架构层面的复用
框架:一组具体类、抽象类、及其之间的连接关系。开发者可以根据spec填充自己的代码从而形成完整的系统。开发者根据Framework预留的接口所写的程序,而Framework作为主程序加以执行,执行过程中调用开发者所写的程序。关于框架详见下一小节4.2.3。
- 黑盒框架:通过实现特定接口/delegation进行框架扩展
- 白盒框架:通过代码层面的继承进行框架扩展
4.2 Construction for Reuse
1. Liskov替换原则(LSP)
子类型多态
子类型多态:客户端可用统一的方式处理不同类型的对象。例:
//Cat是Animal的子类型
Animal a = new Animal();
Animal c1 = new Cat();
Cat c2 = new Cat();
在这段代码中,在任何可以使用a的场景都可以用c1和c2代替a(子类对象取代父类对象)而不会产生任何问题。
LSP
Liskov Substitution Principle中子类重写父类的方法应该满足的条件:
-
编译器在静态类型检查时强制满足的条件
- 子类型可以增加方法,但不可删除
- 子类型需要实现抽象类型中的所有未实现方法
- 子类型中重写的方法返回值必须与父类相同或符合co-variance(协变)
- 子类型中重写的方法必须使用同样类型的参数或者符合contra-variance(逆变)的参数
- 子类型中重写的方法不能抛出额外的异常
-
还应该满足的条件
- 更强的不变量 (RI)
- 更弱的前置条件
- 更强的后置条件
协变
关于返回值的类型,应该保持不变或者变得更具体,也就是与派生的方向一致。
所抛出的异常的类型也是如此。
class T {
Object a() {
… }
void b() throws Throwable {
…}
}
class S extends T {
@Override //返回值从Object协变成了String,这是符合重写的语法的
String a() {
… }
@Override //抛出的异常从Throwable协变成了IOException,这也是符合重写的语法的
void b() throws IOException {
…}
}
逆变
关于参数的类型,应该保持不变或者变得更抽象,也就是与派生的方向相反。
class T {
void c(String s) {
… }
}
class S extends T {
@Override //虽然按照LSP这是合法的,但是在java语法中,不当作override,而是overload
void c(Object s) {
… }
}
类型擦除(泛型中的LSP)
泛型类型是不支持协变的,如ArrayList<String> 是List<String>的子类型,但List<String>不是List<Object>的子类型。这是因为发生了类型擦除,运行时就不存在泛型了,所有的泛型都被替换为具体的类型。
但是在实际使用的过程中是存在能够处理不同的类型的泛型的需求的,如定义一个方法参数是List<E>类型的,但是要适应不同的类型的E,于是可使用通配符?
来解决这个需求:
//无类型条件限制
public static void printList(List<?> list) {
for (Object elem: list)
System.out.print(elem + " ");
}
//限制条件:需要为A类型的夫类型
public static void printList(List<? super A> list){
...}
//限制条件:需要为A类型的子类型
public static void printList(List<? extends A> list){
...}
2. 组合与委托
委派/委托:一个对象请求另一个对象的功能。
一个使用Comparator<T>接口实现delegation的例子:
public class Edge {
Vertex s, t;
double weight;
...
}
public class EdgeComparator implements Comparator<Edge>{
@Override public int compare(Edge o1, Edge o2) {
if(.. > ..) return 1