一、面向复用的软件构造技术
软件构造中的任何实体都有可能被复用。包括需求、规约、测试用例、帮助文档,最主要的复用在代码层面。代码复用分为白盒和黑盒,区别为源码是否可见,是否能在源码上操作修改。根据代码规模,分为如下图4个方面:
1.Source code level
2.Moudle level
继承是Java中天然的复用类的关键字。如果要对父类中方法实现新的功能,采用重写策略。
委托在后面会详细说。
3.Library level
4.Architecture level
框架:一组具体类、抽象类、及其之间的连接关系(接口)。框架作为主程序加以运行,执行过程中调用开发者所写的程序,开发者根据框架预留的接口写程序。白盒框架:通过代码层面的继承实现框架扩展。黑盒框架:通过实现特定的接口或委托进行框架扩展。
二、设计可重用类
1.LSP
LSP原则被提出,目的是保证子类可以扩展父类功能,但不能改变父类原有功能。Java编译器在静态检查阶段增加了如下规则:
- 子类型可以增加方法,但不可删除方法。
- 子类型需要实现抽象类型中的所有未实现方法。
- 子类型中重写的方法必须有 相同或子类型的 返回值。(协变:父类型到子类型越来越具体)(更强的后置条件)
- 子类型中重写的方法必须有同样类型的参数。(逆变:父类型到子类型越来越抽象)(更弱的前置条件)(这里不是相同或父类型的参数是因为java目前会把使用父类型的参数当作overload处理)
- 子类型中某个方法声明的异常应该是父类型方法声明的异常的子类型。
补充一句,规约中使用更强的不变量、更强的后置条件、更弱的前置条件。
2.Array和泛型中的LSP
Array是协变的,这意味着type T[]可以包含任意T类型或者T类型的子类型。
泛型类型是不变的,这意味着列表List<type T>T类型或T类子类变量。尽管Integer是Number的子类,MyClass<Integer> 和 MyClass<Number> 没有继承关系。
通配符类型?表示不确定的类型。List<? super Integer> 表示匹配整数及其父类的任何类型列表。List<? extend Integer> 表示匹配整数及其子类的任何类型列表。使用通配符类型就使不同泛型列表List<>之间有了继承关系。如图:
使用通配符,上例通过静态类型检查。
切记区分List<T>之间的继承关系和T之间的类型关系!!!(举个例子,List<Integer>是List<? extend Integer> 的子类,但是Integer是? extend Integer表示的任意类型的父类。
三、委托
1. Comparable和Comparator
共同点:两者都是接口。都能完成自定义比较某个类的对象。
不同点:假设要为A类设计比较方法,可以让A类直接实现Comparable接口,在A类中重写CompareTo(A a)方法。或者定义一个新的比较器类AComparator实现Comparator接口,在AComparator类中重写compare(A a1,A a2)方法。
2. 委派
创建另一个类的对象,利用这个对象请求这个对象类的方法。但一个类需要使用另一个类的一小部分方法时,采用委派而不是继承,从而避免大量无用的方法和类之间的耦合。
委派发生在Objects层面,而继承发生在Class层面。当一个方法对每个对象来说足够个性化,那么就用委派而不是继承。如果一个方法是某个类所有对象的通用方法,那么用继承而不是委派。
下面是一个计算员工奖金的具体实现逻辑。计算奖金对于每个员工来说就是一个个性极强的方法,从以下例子可以理解委托的应用。
下面这个例子模拟动物的不同行为。因为动物的行为极具多样性,所以用接口定义抽象行为。每种动物实现某些接口的组合,行为的具体实现仍然委托给具体实现类,原因与上例相同。
委托的类型:
1)临时性委托dependence:一个类使用另一个类,但并没有把这个类的变量放在字段中。另一个类作为参数出现在这个类中。
2)永久性委托association:一个类使用另一个类,并把另一个类的变量当作自己的字段。
3)更强的永久性委托composition:直接把另一个类的对象赋给字段中的类变量。
4)更弱的永久性委托aggregation:另一个类的类对象在外部,通过set方法赋予字段中的类变量。