目标:
- 从类、API、框架三个层面学习如何设计可复用软件实体的具体技术
- 设计可复用的类
- 设计可复用的框架
1 What is Software Reuse?
面向复用编程:开发出可复用的软件
基于复用编程:利用已有的可复用软件搭建应用系统
可重用的组件需要(清晰的定义、开放模式、简洁的接口、易于理解的文档),做到这些,需要代价。
不仅program for reuse代价高,program with reuse代价也高
开发成本高于一般软件的成本:要有足够高的适应性
性能差些:针对更普适场景,缺少足够的针对性
使用已有的软件开发:
可复用软件库,对其进行有效的管理
adaptation 往往无法拿来就用,需要适配
2 How to measure “reusability”?
3.复用模块的层次和形态学模式
最主要的复用是在代码层面,但软件构造中任何实体都可能被复用
源代码层次:方法
模块层次:类和接口
库层次:API
架构层次:框架
白盒复用:源代码可见,可修改和扩展
- 复制已有代码到正在开发的系统,进行修改
- 可定制化程度高
- 对其修改增加了软件的复杂度,且需要对其内部充分的了解
黑盒复用:源代码不可见,不能修改
- 只能通过API接口来使用,无法修改代码
- 简单,清晰
- 适用性差
源代码复用:最低级层次
模块层次的复用:类和接口
复用classes
重用一个类的方法:inheritance继承
- 继承是一种复用模式,子类可以复用父类的方法,也可以重写
- 通常需要在实现之前设计继承层次结构
- 无法取消属性或方法,因此必须小心不要过度使用
重用一个类的另一种方法:delegation委托
委派只是一个对象依赖另一个对象来实现其功能的某个子集。
3.库层次的复用:API/Package
库:提供可重用功能的类和方法(API)
框架:可自定义到应用程序中的可重用骨架代码
4.系统层次的复用:框架
框架:一组具体类、抽象类、及其之间的连接关系
开发者根据framework的规约,填充自己的代码进去,形成完整系统
将framework看作是更大规模的API复用,除了提供可复用的API,还将这些模块之间的关系都确定下来,形成了整体应用的领域复用。
抽象的层次是不同的,
因为框架为一系列相关问题提供了解决方案,而不是单个问题。
为了适应这一系列的问题,这个框架是不完整的,包含了热点和钩子以允许定制。
白盒框架:通过代码层面的继承进行框架扩展
黑盒框架:通过实现特定接口/delegation进行框架扩展。
- 通过定义可插入到框架中的组件的接口来实现的可扩展性。
- 通过定义符合特定接口的组件来重用现有的功能。
- 这些组件通过委托与框架进行了集成。
5设计可重用的类
1.行为子类型和里氏替换原则
行为子类性
子类型多态:客户端可用统一的方式处理不同类型的对象。
里氏替换原则:
第一种定义:如果对每一个类型为S的对象o1,都有类型为T的对象o2,使得以T定义的所有程序P在所有的对象o1都代换成o2时,程序P的行为没有发生变化,那么类型S是类型T的子类型。
第二种定义:所有引用基类的地方必须能透明地使用其子类的对象。
§ 1、子类可以实现父类的抽象方法,但不能覆盖父类的非抽象方法;
§ 2、子类中可以增加自己特有的方法
- 子类型可以增加方法,但不可删
- 子类型需要实现抽象类型中的所有未实现方法
- 子类型中重写的方法必须有相同或子类型的返回值或者符合co-variance的参数
- 子类型中重写的方法必须使用同样类型的参数或者符合contra-variance的参数
- 子类型中重写的方法不能抛出额外的异常
LSP: 规约不变或更强
LSP 强行为子类型化
Covariance (协变)
父类型子类型:越来越具体specific
返回值类型:不变或变得更具体
异常的类型:也是如此。
class T {
Object a() { … }
}
class S extends T {
@Override
String a() { … }
}
class T {
void b( ) throws Throwable {…}
}
class S extends T {
@Override
void b( ) throws IOException {…}
}
class U extends S {
@Override
void b( ) {…}
}
-为子类型的方法声明的每个异常都应该是为超类型的方法声明的某个异常的子类型。
Contravariance (反协变、逆变)
父类型子类型:越来越具体specific
参数类型:要相反的变化,要不变或越来越抽象
目前Java中遇到这种情况,当作overload看待。
数组是协变的:
Number[] numbers = new Number[2];
numbers[0] = new Integer(10);
numbers[1] = new Double(3.14);
Integer[] myInts = {1,2,3,4};
Number[] myNumber = myInts;
myNumber[0] = 3.14; //run-time error!
myNumber是引用类型,而实际是Interger类型。
区分:对象类型与引用类型。
泛型中的LSP
泛型是类型不变量:
– ArrayList< String > is a subtype of List< String >
– List< String > is not a subtype of List< Object >
编译代码完成后,编译器丢弃类型参数的类型信息;因此,此类型信息在运行时不可用。——类型擦除
泛型类型不是协变的。
类型擦除:
如果泛型类型中的类型参数是无限制的,将被对象类型参数所替换。因此,生成的字节码只包含普通类、接口和方法。
Box< Integer >不是Box< Number >的子类型,即使Integer是Number的子类型。
A与B有关系,Box(A)与Box(B)没有关系。
泛型编程中的通配符:
使用通配符指定了无限的通配符类型(?),例如,List<?>。
在以下两种情况下,无界通配符是一种有用的方法:
- 如果您要编写一种可以使用对象类中提供的功能来实现的方法。
- 当代码使用泛型类中不依赖于类型参数的方法时。例如,List.size或List.clear.
- 实际上,使用类<?>非常频繁,因为类中的大多数方法都不依赖于T。
public static void printList(List<Object> list) {
for (Object elem : list)
System.out.println(elem + " ");
System.out.println();
}
它只能打印对象列表,不能打印 List< Integer >, List< String >, List< Double >, 等 因为它们不是List的子类型。
public static void printList(List<?> list) {
for (Object elem: list)
System.out.print(elem + " ");
System.out.println();
}
List<Integer> li = Arrays.asList(1, 2, 3);
List<String> ls = Arrays.asList("one", "two", "three");
printList(li);
printList(ls);
上线通配符extends
下限通配符super
public static double sumOfList(List<? extends Number> list) {
double s = 0.0;
for (Number n : list)
s += n.doubleValue();
return s;
}
List<Integer> li = Arrays.asList(1, 2, 3);
List<Double> ld = Arrays.asList(1.2, 2.3, 3.5);
继承复用的优缺点
§ 通常类的复用分为继承复用和合成复用两种,继承复用虽然有简单和易实现的优点。
§ 继承复用破坏了类的封装性。因为继承会将父类的实现细节暴露给子类,父类对子类是透明的,所以这种复用又称为“白箱”复用。
§ 子类与父类的耦合度高。父类的实现的任何改变都会导致子类的实现发生变化,这不利于类的扩展与维护。
§ 它限制了复用的灵活性。从父类继承而来的实现是静态的,在编译时已经定义,所以在运行时不可能发生变化
(2)委托与组成
Interface Comparator< T >
如果你的ADT需要比较大小,或者要放入Collections或Arrays进行排序,可实现Comparator接口并override compare()函数。
public class Edge {
Vertex s, t;
double weight;
...
}
public class EdgeComparator
implements Comparator<Edge>{
@Override
public int compare(Edge o1, Edge o2) {
if(o1.getWeight() > o2.getWeight())
return 1;
else if (.. == ..) return 0;
else return -1;
}
}
public void sort(List<Edge> edges) {
Comparator comparator = new EdgeComparator();
Collections.sort(edges, comparator);
}
Interface Comparable< T >
public class Edge implements Comparable<Edge> {
Vertex s, t;
double weight;
...
public int compareTo(Edge o) {
if(this.getWeight() > o.getWeight())
return 1;
else if (.. == ..) return 0;
else return -1;
}
}
委派/委托:一个对象请求另一个对象的功能
委派是复用的一种常见形式
class A {
void foo() {
this.bar();
}
void bar() {
print("a.bar");
}
}
class B {
private A a; // delegation link
public B(A a) {
this.a = a;
}
void foo() {
a.foo(); // call foo() on the a-instance
}
void bar() {
print("b.bar");
}
}
B b = new B(a); // establish delegation between two objects
委派模式:通过运行时动态绑定,实现对其他类中代码的动态复用。
设计经常采用继承和委托结合的方式
如果子类只需要复用父类中的一小部分方法,可以不需要使用继承,而是通过委派机制来实现。
CRP组合重用原则
类通过“组合”实现多态和复用(通过引入其他类的实例来实现功能)而不是通过基类或父类。
.“委托”发生在object层面,而“继承”发生在class层面。