软件构造4.2面向复用的软件构造技术笔记

Chapter4:面向软件构造可复用性(Reusability)的方法

4.2面向复用的软件构造技术(Construction for reuse)

1.设计可复用的类

在oop中设计可复用的类:

已经介绍过的:

  • 封装和信息隐藏
  • 遗传和重写
  • 多态,子类型,重载
  • 泛型编程

还未介绍过的:

  • 行为子类型(Behavioral subtyping)和Liskov替换原则(LSP)
  • 委托(delegation)和组合(composition)

(1)行为子类型和Liskov替换原则(Liskov Substitution Principle)

行为子类型:
子类型多态:客户端可用统一的方式处理不同类型的对象。
Barbara Liskov:对于类型T的对象x,q(x)成立,那么对于类型T的子类型S的对象y,q(y)也成立。

软件构造4.2面向复用的软件构造技术笔记
子类型的要求:(编译加强原则)(static type checking)

  1. 子类型可以增加方法,但不可删
  2. 子类型需要实现抽象类型(接口.抽象类)中所有未实现的方法
  3. 子类型中重写的方法必须有相同或子类型的返回值或者co-variant的参数
  4. 子类型中重写的方法必须使用同样类型的参数或者符合contra-variant的参数
  5. 子类型的方法不能抛出额外的异常。

规约的行为:
1.更强的不变量
2.更弱的前置条件
3.更强的后置条件

Liskov替换原则:
1.前置条件不能强化
2.后置条件不能弱化
3.不变量要保持
4.子类型方法参数:逆变
5.子类型方法的返回值:协变
6.异常类型:协变

协变(Covariance)
父类型到子类型:越来越具体
返回值的类型:越来越具体
异常的类型:越来越具体

例子:返回值的类型更具体。
软件构造4.2面向复用的软件构造技术笔记
例子:抛出的异常是父类型异常的子异常。
软件构造4.2面向复用的软件构造技术笔记
反协变,逆变(Contravariance)
父类型到子类型:越来越具体
参数的类型:不变或越来越抽象

例子:应该把@Override去掉,因为参数发生了改变不是重写,而是重载。参数变得越来越抽象。
软件构造4.2面向复用的软件构造技术笔记
用一张图总结:其中T‘是T的子类。
软件构造4.2面向复用的软件构造技术笔记
泛型中的LSP:

  • ArrayList是List的子类型
  • List和List没有任何关系

Class类型类:

Java在运行时,为所有对象维护一个运行时类型标识,这个标识跟踪对象所属的类,用来确定选择哪个方法运行。保存这些信息的类叫做“Class类型类”。注意:Class是类的名字,不是关键词class。每个”Class”的对象描述了一个类的信息。

例子:
软件构造4.2面向复用的软件构造技术笔记
类型擦除:(type erasure)
类型擦除:类型参数在编译后被丢弃,在运行时不可用。

泛型信息只存在于编译阶段,在运行时会被擦除。
定义泛型类型时,会自动提供一个对应的原始类型(非泛型类型),原始类型的名字就是去掉类型参数后的泛型类型名。

擦除时类型变量会被擦除,替换为限定类型,如果没有限定类型则替换为Object类型。

*没有限定类型:*替换为Object。
软件构造4.2面向复用的软件构造技术笔记
加粗样式
限定类型:有多个限定时,用第一个,当有限定时可以调用T的方法。
软件构造4.2面向复用的软件构造技术笔记
**类型查询:**运行时类型查询只适用于原始类型
在例子中:Pair和Pair的类是相同的。
软件构造4.2面向复用的软件构造技术笔记
两个例子证明Pair和Pair不能互相转换:
*例子1:*证明了Listmynums=List myints编译会报错。
软件构造4.2面向复用的软件构造技术笔记
*例子2:*证明了List都不是List的子类。如果编译通过会出现致命错误。

软件构造4.2面向复用的软件构造技术笔记
软件构造4.2面向复用的软件构造技术笔记
泛型中的LSP:
Integer是Number的子类,但Box和Box的父类是Object。
软件构造4.2面向复用的软件构造技术笔记

通配符:(Wildcards)
可采用通配符实现两个泛类型的协变。

本意期望可打印任意类型的List,但由于泛型不协变,只能打印List。
软件构造4.2面向复用的软件构造技术笔记

将Object改为?就可以对任意类型通用。
软件构造4.2面向复用的软件构造技术笔记

下限通配符:<? super A>
?可为A或A的父类。

上限通配符:<? extends A>
?可为A或A的子类。

注意:!!!!无论?是什么,注入的数据可为?,或为?的子类,与super还是extends A无关。

图示:
?包括所有类,所以Number和Integer为?的子类
软件构造4.2面向复用的软件构造技术笔记
左侧:?extends Number Integer 说明?越来越小,为子类
右侧:?super Integer Number,也说明?越来越小,为子类。
软件构造4.2面向复用的软件构造技术笔记
PECS(选学)produce-extends,consumer-super(also named Get and Put Principle)

producer-extends:

如果你的ADT需要比较大小,或者要放入Collections或Arrays中进行排序,可实现Comparator接口并override compare()函数。因为?为Cat的一种子类,但不知道?是哪个,可能为最小的,而使whiteCat不为?的子类
软件构造4.2面向复用的软件构造技术笔记
无论注入的元素为什么,它一定是?或?的子类,而?是Cat的子类,所以该元素一定是Cat或Cat所有父类的子类。
软件构造4.2面向复用的软件构造技术笔记
consumer-super:
?一定是Cat的父类,所以注入任何Cat或Cat的子类元素都不会有问题。
软件构造4.2面向复用的软件构造技术笔记
因为?可以取Object,所以也有可能注入了Object元素,接收的时候,只能用Object接收。

软件构造4.2面向复用的软件构造技术笔记
(2)委托和组合(Delegation and composition)
Interface Comparator

如果你的ADT需要比较大小,或者要放入Collections或Arrays中进行排序,可实现Comparator接口并override compare()函数。

*方法一:*实现Comparator接口并override compare()函数。
例子:Edge函数,想将其中的元素按照weight排序。
软件构造4.2面向复用的软件构造技术笔记
重写Edge的cmp函数。
软件构造4.2面向复用的软件构造技术笔记

将元素加入list中,然后将比较器代入,重写toString函数,可输出weight从小到大的排序。
软件构造4.2面向复用的软件构造技术笔记
*方法2:*让你的ADT实现Comparable接口,然后override compareTo() 方法。
软件构造4.2面向复用的软件构造技术笔记
委托(Delegation)
委托/委派:一个对象请求另一个对象的功能。

例子:这里类B中有一个类A中的元素,B中的方法可通过调用B中元素a的方法来实现。(从而不用再在b中实现相同的方法,提高了复用性,降低了耦合度)

软件构造4.2面向复用的软件构造技术笔记

委托的优点:

  1. 子类可使用父类中的一小部分方法
  2. 可以不使用继承,而是通过委派机制来实现。
  3. 避免继承大量无用的方法。

组合复用原则(Composite Reuse Principle)(CRP)
“委托”发生在object层面,而“继承”发生在class层面。

CRP的例子:
*例子1:*不同的员工需要分别计算他们的奖金,如总经理,程序员,秘书。
最简单的方式:不断使用继承,重写计算奖金函数。
软件构造4.2面向复用的软件构造技术笔记
软件构造4.2面向复用的软件构造技术笔记
软件构造4.2面向复用的软件构造技术笔记
面临的问题:

  1. 不同类型的额manager需要不同的计算方式,需要引入子类。
  2. 更改某manager的类型时很难处理。
  3. 如果两个类型的计算方式一样时,可能需要复制,更改时耦合度高。

核心问题:每个Emplyee对象的奖金计算方法都不同,在object层面而非class层面。

*例子2:*一种CRP的解决方法。将计算奖金和人分开,写成两个类,奖金和人分别继承自己的父类,而人要计算奖金时通过委托奖金的类计算。
软件构造4.2面向复用的软件构造技术笔记
这种方法的示意图如下:
软件构造4.2面向复用的软件构造技术笔记
*例子3:*更普适的CRP。
假设开发一套动物ADT,行为是飞,叫。如天鹅,鸭子,狗(不会飞)。

最简单的方式:直接面向具体类型动物的编程。(不好)
软件构造4.2面向复用的软件构造技术笔记
问题:存在大量的重复,不易变化。

另一种通过继承的方式:实现对某些通用行为的复用,该例子为飞行。(不好)
软件构造4.2面向复用的软件构造技术笔记
缺点:需要针对“飞行”设计复杂的继承关系树;不能同时支持针对“叫法”的继承;动物行为发生变化时,继承树要随之变化。

最好的CRP方式:
软件构造4.2面向复用的软件构造技术笔记

软件构造4.2面向复用的软件构造技术笔记
委托的三种类型:
1.Dependency (A use B)
2.Association (A has B)
3.Composition/aggregation (A owns B) 可以认为Composition/Aggregation是Association的两种具体形态

1.Dependency临时性的delegation
通过方法的参数或者在方法的局部中使用发生联系。
Duck中没有属性,而是靠传入的参数来决定Duck的飞行方法。
软件构造4.2面向复用的软件构造技术笔记
2.Association: 永久性的delegation
Duck中有field,通过Duck()成员变量传入参数,来决定Duck的飞行方式。
软件构造4.2面向复用的软件构造技术笔记

3.Composition: 更强的association,但难以变化
相当于已经设定好了Duck的飞行方式,但无法更改。
软件构造4.2面向复用的软件构造技术笔记

4.Aggregation: 更弱的association,可动态变化
Duck中有field,可以通过set来设定飞行行为。
软件构造4.2面向复用的软件构造技术笔记