面向复用的软件构造技术
- LSP原则-Liskov替换原则
-
- 协变 Co-variance
- 通配符
- 逆变 Contra-variance
- 比较
-
- Comparator< T>
- Comparable< T>
- Delegation 委派
-
- CRP
- 委派的四种形式
- 白盒框架
- 黑盒框架
LSP原则-Liskov替换原则
一.基本内容(子类型多态)
1.子类型可以增加方法,但不可删
2.子类型需要实现抽象类型中的所有未实现方法
3.子类型中重写的方法必须有相同或子类型的返回值或者符合co-variance的参数
4.子类型中重写的方法必须使用同样类型的参数或者符合contra-variance的参数
5.子类型中重写的方法不能抛出额外的异常
6.子类方法具有更强的不变量
7.子类方法具有更弱的前置条件
8.子类方法具有更强的后置条件
二.实例
1.具有更强的不变量
2.更弱的前置条件和更强的后置条件
3.不符合条件的方法,调用GraphicProgram中的scaleW方法可能会导致h不等于w,破坏表示不变性
4.不兼容的spec:
三.满足条件
1.前置条件不能强化
2.后置条件不能弱化
3.不变量要保持
4.子类型方法参数:逆变
5.子类型方法的返回值:协变
6.异常类型:协变
协变 Co-variance
父类型->子类型:越来越具体specific
返回值类型:不变或变得更具体
异常的类型:不变或变得更具体
对于子类,更特定的类可能有更特定的返回类型
为子类型的方法声明的每个异常都应该是为超类型的方法声明的某些异常的子类型。
数组是协变的
给定Java的子类型规则,T类型的数组[]可以包含T类型的元素或T的任何子类型。
泛型不是协变的
Box< Integer >不是Box< Number >的子类型,即使Integer是Number的子类型。
给定两个具体类型A和B(例如,Number和Integer),无论A和B是否相关,MyClass< A>与MyClass< B >没有关系。MyClass< A>和MyClass< B>的共同父类是Object。
通配符
一.使用通配符(?)指定无限制通配符类型,例如List< ?>
二.如下情况通常使用通配符:
1.编写一个可以使用Object类中提供的功能实现的方法。
2.当代码在泛型类中使用不依赖于类型参数的方法时。例如,List.size或List.clear。
实际上,Class< ?>之所以如此常用,是因为Class中的大多数方法不依赖于T。
三.上下界
1.低有界通配符< ? super A>
List< Interger>只匹配Integer类型的列表
List< ? super Integer>匹配Integer的超类型的任何类型的列表,如Integer、Number和Object。
2.上有界通配符< ? extends A>
总结来说
逆变 Contra-variance
父类型->子类型:越来越具体specific
参数类型:要相反的变化,要不变或越来越抽象
比较
Comparator< T>
若ADT需要比较大小,可实现Comparator接口并override compare()函数
举例来说
//首先实现一个Edge类
public class Edge {
double weight;
public Edge(double w) {
weight = w;
}
public double getWeight() {
return weight;
}
public String toString() {
return "权重为"+weight;
}
}
public static void main(String[] args) {
List<Edge> list = new LinkedList<>();
list.add(new Edge(5.0));
list.add(new Edge(4.5));
list.add(new Edge(3.9));
list.add(new Edge(8.7));
//使用Comparator接口进行比较
Collections.sort(list, new Comparator<Edge>() {
@Override
public int compare(Edge o1, Edge o2) {
if(o1.getWeight() > o2.getWeight()) return 1;
else return -1;
}
});
System.out.println(list);
}
输出结果
Comparable< T>
另一种方法:让ADT实现Comparable接口,然后override compareTo()方法
与Comparator的区别:不需要构建新的Comparator类,比较代码放在ADT内部。
举例来说
// 实现Comparable接口,重写compareTo方法
public class Edge implements Comparable<Edge>{
double weight;
public Edge(double w) {
weight = w;
}
public double getWeight() {
return weight;
}
public String toString() {
return "权重为"+weight;
}
@Override
public int compareTo(Edge o) {
if(this.getWeight()>o.getWeight()) return 1;
else return -1;
}
}
public static void main(String[] args) {
Edge[] edge = new Edge[] {
new Edge(5.0),
new Edge(4.5),
new Edge(3.9),
new Edge(8.7)
};
Arrays.sort(edge); // 使用Arrays.sort()方法进行排序
System.out.println(Arrays.toString(edge));
}
Delegation 委派
委派/委托:一个对象请求另一个对象的功能
与继承的区别
继承:通过新操作或重写操作来扩展基类。
委派:捕获一个操作并将其发送给另一个对象。
如果子类只需要复用父类中的一小部分方法,可以不需要使用继承,而是通过委派机制来实现。
一个类不需要继承另一个类的全部方法,通过委托机制调用部分方法,从而避免大量无用的方法。
尽量用delegation代替inheritance
CRP
类应该通过它们的组合(通过包含实现所需功能的其他类的实例)来实现多态行为和代码重用,而不是从基类或父类继承。
组合一个对象可以做什么(has_a或use_a)比扩展它是什么(is_a)要好。
“委托” 发生在object 层面,而“继承”发生在class 层面
举例:
使用CRP原则实现
使用接口实现公共的方法但不同的实现
通过接口的组合定义行为的组合,从组合接口中派生具体类,在类中进行委派,可以通过委派实现对象实例和方法的调用
委派的四种形式
1.Dependency: 临时性的delegation
使用类的最简单形式是调用它的方法;两个类之间的这种形式的关系称为“use -a”关系,在这种关系中,一个类利用另一个类,而没有实际将它作为属性合并。
例如,它可以是一个参数,也可以在方法中本地使用。依赖关系:一个对象的实现需要其他对象(供应商)的临时关系。
2.Association: 永久性的delegation
关联:对象类之间的持久关系,允许一个对象实例导致另一个对象实例代表它执行一个操作。
has_a:一个类有另一个类作为属性/实例变量-这种关系是结构性的,因为它指定了一种对象连接到另一种对象,而不代表行为。
3.Composition: 更强的association,但难以变化
组合是一种将简单对象或数据类型组合成更复杂的对象或数据类型的方法。is_part_of:一个类有另一个类作为属性/实例变量。
4.Aggregation: 更弱的association,可动态变化
聚合:对象存在于另一个对象之外,在另一个对象之外创建,因此它被作为参数传递给构造函数。
总结
白盒框架
通过subclassing和overriding方法扩展
1.允许每个非私有方法的扩展
2.需要理解超类的实现
3.一次只有一个扩展
4.一起编译
5.通常是所谓的开发框架
黑盒框架
通过委派和接口组合实现方法
1.允许在接口中公开功能的扩展
2.只需要了解接口
3.多个插件
4.经常提供更多的模块化
5.独立的部署
6.通常是所谓的终端用户框架,平台
总结