Note6- 面向复用 (reuse) 的软件构造技术

前几章介绍了软件构造的核心理论——ADT,核心技术——OOP,其核心是保证代码质量、提高代码安全性

本章面向一个重要的外部指标:可复用性——如何构造出可在不同应用中重复使用的软件模块/API

为什么复用?

软件复用有两个视角:

三点好处:

但是,代价也不低:

开发可复用的软件一般流程如下:

使用已有软件进行开发的一般流程:

怎么复用?

源码层面的复用

说白了就是搜索相应的代码。复制过来为自己所用

模块层面的复用:类/接口

使用继承委托

Library 层面的复用:API/包

Library: 提供可复用功能的类和方法的集合

系统层面的复用:框架 (Framework)

所谓框架,就是一组具体类、抽象类、及其之间的连接关系

开发者根据框架的规约,填充自己的代码进去,形成完整系统

框架分为两种:

设计可复用的类

LSP 原则 (Liskov Substitution Principle)

子类型多态:使用者可以用统一的方式处理不同类型的对象

看如下代码:

Animal a = new Animal();
Animal c1 = new Cat();
Cat c2 = new Cat();

要保持这样一种原则:

在任何可以使用 a 的场景,都可以用 c1 和 c2 替换而不会有任何问题

总结来说,就是以下七点:

  1. 子类必须完全的实现父类的方法(子类型需要实现抽象类中的所有未实现的方法)
  2. 子类可以有自己的个性(子类型可以增加方法)
  3. 子类型方法参数:逆变,返回值:协变
  4. 子类型中重写的方法不能抛出额外的异常:协变
  5. 重写和实现父类的方法时输入参数可以被放大(前置条件不能强化/逆变)
  6. 重写和实现父类的方法时输出参数可以被缩小(后置条件不能弱化/协变)
  7. 更强/保持不变量

协变 (Covariance)

所谓协变就是无论是父类型到子类型还是方法的返回值类型还是异常的类型都越来越具体

比如:

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 {...}
}

反协变 (Contravariance)

反协变是指从父类型到子类型越来越具体,方法的参数类型相反,要不变或越来越抽象

比如:

class T {
    void c(String s) {...}
}
class S extends T {
    @Override
    void c(Object s)
}

在 Java 中,这种情况被看作 Overload

总结如图:

泛型中的 LSP

先说说类型擦除(type erasure)

举例,类型参数没有限定时:

类型参数有限定时:

由此明确一点:

类似的,Box<Integer> 不是 Box<Number> 的子类型:

那么两个泛型类的协变如何实现呢?

通配符 (Wildcards)

可以采用通配符(Wildcards)

无限定通配符? 表示,在以下情况使用

比如,我们想设计一个方法,打印任意类型 List 中所有内容

public static void printList(List<Object> list) {
    for (Object elem : list)
	System.out.println(elem + " ");
	System.out.println();
}

由于泛型不协变,这个时候只能打印 List<Object>

但是有了通配符就好办了:

public static void printList(List<?> list) {
    for (Object elem : list)
	System.out.println(elem + " ");
	System.out.println();
}

除此以外,还有下限通配符<? super A>

指只能接受类型 A 以及 A 的父类作为类型参数

上限通配符<? extends A>

指只能接受类型 A 以及 A 的子类作为类型参数,这里的 extends 既可以代表类的 extends,也可以代表接口的 implements

比如,可以写出对存放数的 List 的求和方法:

public static double sumofList(List<? extends Number> list) {
    double s = 0.0;
    for (Number n : list)
        s += n.doubleValue();
    return s;
}

也可以有多种限定的写法:

<T extends B1 & B2 & B3> 表示接受的类型参数要是后面所有类的子类型,由于 Java 不能多继承,所以 B1,B2,B3 中最多只能有一个类,剩下的都是接口,要把类写在最前面

总结如图:

PECS

PECS 就是 producer-extends, consumer-super

举例:

producer-extends

class Animal{}
class Cat extends Animal{}
class whiteCat extends Cat{}
class BlackCat extends Cat{}

List<? extends Cat> animals= new ArrayList<Cat>();
animals.add(new whiteCat()); //compile error
animals.add(new Cat()); //compile error
animals.add(new Animal()); //compile error
animals.add(new Object()); //compile error
animals.add(null); //succeed, but it is meaningless.
//不能放入任何类型,因为编译器只知道animals中应该放入Cat的某种子类型,但具体放哪种子类型它无法确定
animals= new ArrayList<WhiteCat>();
…
animals= new ArrayList<BlackCat>();
//假如允许放入WhiteCat后,animals可能指向BlackCat集合;反之亦然。
Cat s1 = animals.get(0); //类型上界为Cat,Cat及其父类都能接收返回值
Animal s2 = animals.get(0); //Cat类型可以用Animal接收
WhiteCat s3 = animals.get(0); //error:子类型对象不能接收父类型返回值

consumer-super:

class Animal{}
class Cat extends Animal{}
class whiteCat extends Cat{}
class BlackCat extends Cat{}

List<? super Cat> b = new ArrayList<>(); //参数类型下界是Cat
b.add(new Cat()); //ok
b.add(new WhiteCat()); //ok 子类型也可以
b.add(new Animal()); //error 超类不可以
b.add(null); //ok

Object o1 = b.get(0);//返回类型是未知的,只能用Object类型接收

委托 (delegation)

举个排序的例子:

这个例子可以看到 ADT 中比较大小的一种方式,可以实现 Comparator 接口,并且重写 compare() 函数

所谓委托(delegation) 就是一个对象请求另一个对象的功能

委托是复用的一种常见形式,它可以描述为一种低级的代码与数据的共享机制

再举个简单的例子:

B 通过一个 A 的成员变量,构建起两个类之间的委托,这时显式的

再比如,我们要实现一个能在添加或删除时打印信息的 List,使用委托机制代码就很简洁了

委托 (delegation) 与继承 (inheritance)

写到这里,谈谈委托与继承的区别

委托能办到的,继承似乎都能办到,那么为什么不直接使用继承呢?

譬如,如果子类只需要复用父类的一小部分方法,完全可以不需要继承,而是通过委托机制来实现,从而避免继承大量无用的方法

合成复用原则 (CRP)

内容:

也就是说,组合要优先于继承(组合式委托的一种形式)

注意:委托发生在对象的层面,而继承发生在类的层面

那么为什么在对象层面更好呢?举个例子:

Employee 类有一个方法用于计算奖金:

class Employee {
    Money computeBonus() {... // default computation}
}

它会有很多不同的子类,例如 Manager,Programmer,Secretary,那么计算它们的奖金的时候肯定要重写方法:

class Manager extends Employee {
    @Override
    Money computeBonus() {... // special computation}
}

如果不同类型的 manager 需要不同的计算方式,那么就有需要引入子类:

class SeniorManager extends Manager {
    @Override
    Money computeBonus(){... // more special computation}
}

如果要将某个人从 Manager 提升为 SeniorManager,那么该怎么处理呢?

核心问题在于:每个 Employee 对象的奖金计算方法都不同,这在对象层面而不是层面

显然,委托机制要更好,使用 CRP 原则的一种实现可以是:

class Manager {
    ManagerBonusCalculator mbc = new ManagerBonusCalculator();
    Money computeBonus() {
        return mbc.computeBonus();
    }
}

class ManagerBonusCalculator {
    Maney computeBonus {... // special computation}
}

设计如图:

举例

再举个例子,能够清晰地展现出从继承到委托的变化

假设要开发一套动物 ADT

例如:

第一种实现方式:可以直接为每一种动物都设置一个类:

缺陷很明显,很多动物的飞法其实是一样的,会存在大量重复

第二种实现方式:利用继承,比如先定义一个能飞的动物的抽象类,实现通用的的飞法,每种能飞的动物类都继承这个类,如果有不同的飞法重写即可

这样做实现了对某些通用行为的复用,但是也有缺陷

第三种实现方式:利用组合

思路:

这样就能规避复杂的继承关系

比如分别设计抽象行为的接口:

interface Flyable {
    public void fly();
}
interface Quackable {
    public void quack();
}

然后将接口组合,也就是行为的组合,比如:

interface Ducklike extends Flyable, Quackable{}

然后再在具体的类中实现这个接口

一种实现如图:

委托的类型

三种形态:

使用者调用某个功能也就呈现出这样的结构:

接下来,我们逐一分析:

Dependency: 临时性的委托

Dependency: a temporary relationship that an object requires other objects (suppliers) for their implementation.

举例

Association: 永久的委托

Association: a persistent relationship between classes of objects that allows one object instance to cause another to perform an action on its behalf.

举例

Composition: 更强的联系

Composition is a way to combine simple objects or data types into more complex ones.

但是这种实现难以变化

举例

Aggregation: 更弱的联系

Aggregation: the object exists outside the other, is created outside, so it is passed as an argument to the construtor.

这种实现可以动态变化

举例

设计系统层面的 API 库与框架 (frameworks)

可以说,API 是一个程序员最重要的资产和荣耀

白盒 (Whitebox) 框架与黑盒 (Blackbox) 框架

白盒框架

黑盒框架

举例

举一个计算器的例子:

不用框架

使用白盒框架

使用黑盒框架

Whitebox vs. Blackbox Frameworks

总结

本文从四个层面(源码级别,模块级别,库级别,系统级别)分别讲解了如何设计复用

尤其分析了设计可复用的的方法,从中导出著名的 LSP 原则和 CRP 原则