4.2 面向复用的软件构造技术
一. 设计可复用的类
- 继承与重写
- 重载
- 参数多态与泛型编程
- 行为子类型与
Liskov
替换原则 - 组合与委托
1.1 LSP
原则
subtying
:若 B
继承 A
,则 B
为 A
的子类型,即任意 B
均为 A
,A
能做的事,B
也要能做。又称为行为 subtyping
。
Animal a = new Animal();
Animal c1 = new Cat();
Cat c2 = new Cat();
在可以使用 a
的场景,都可以用 c1
和 c2
代替而不会有任何问题:
a = c1;
a = c2;
子类型多态:客户端可用统一的方式处理不同类型的对象。
检查 B
是 A
的子类型的合理性(LSP
原则):(前五种静态检查,后三种编译器无法检查)
- 子类型可以增加方法,但不可删
- 子类型需要实现抽象类型中的所有未实现方法
- 子类型中重写的方法必须有相同或子类型的返回值或者符合
co-variance
的参数(子类型的返回类型是父类型返回类型的子类型,协变) - 子类型中重写的方法必须使用同样类型的参数或者符合
contra-variance
的参数(子类型的参数类型是父类型参数类型的父类型,异变) - 子类型中重写的方法不能抛出额外的异常(协变)
- 更强的不变量
- 更弱的前置条件
- 更强的后置条件
总之:
- 父类型的不变量,子类型满足(
RI
必须完整继承下来)或者更强(对新的属性增加不变量) - 子类型的方法强度大于等于父类型(子类型实现的功能多于父类型)
如下两个程序:
父类型:
abstract class Vehicle {
int speed, limit;
//@ invariant speed < limit;
//@ requires speed != 0;
//@ ensures speed < \old(speed)
void brake();
}
子类型:
class Car extends Vehicle {
int fuel;
boolean engineOn;
//@ invariant speed < limit;
//@ invariant fuel >= 0;
//@ requires fuel > 0 && !engineOn;
//@ ensures engineOn;
void start() { … }
void accelerate() { … }
//@ requires speed != 0;
//@ ensures speed < \old(speed)
void brake() { … }
}
如下两个程序:
父类型:
class Car extends Vehicle {
int fuel;
boolean engineOn;
//@ invariant speed < limit;
//@ invariant fuel >= 0;
//@ requires fuel > 0 && !engineOn;
//@ ensures engineOn;
void start() { … }
void accelerate() { … }
//@ requires speed != 0;
//@ ensures speed < \old(speed)
void brake() { … }
}
子类型:
class Hybrid extends Car {
int charge;
//@ invariant charge >= 0;
//@ requires (charge > 0 || fuel > 0) && !engineOn;
//@ ensures engineOn;
void start() { … }
void accelerate() { … }
//@ requires speed != 0;
//@ ensures speed < \old(speed)
//@ ensures charge > \old(charge)
void brake() { … }
}
如下两个程序(不合理):
父类型:
class Rectangle {
//@ invariant h>0 && w>0;
int h, w;
Rectangle(int h, int w) {
this.h=h; this.w=w;
}
//methods
//@ requires factor > 0;
void scale(int factor) {
w=w*factor;
h=h*factor;
}
//@ requires neww > 0;
//@ ensures w=neww && h not changed
void setWidth(int neww) {
w=neww;
}
}
子类型
class Square extends Rectangle {
//@ invariant h>0 && w>0;
//@ invariant h==w;
Square(int w) {
super(w, w);
}
//@ requires neww > 0;
//@ ensures w=neww && h=neww
@Override
void setWidth(int neww) {
w=neww;
h=neww;
}
}
协变:
- 父类型 → 子类型:越来越具体
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( ) {…}
}
数组支持协变(二三行),但 Java
不提供泛型数组,这是因为在泛型中需要实现泛型擦除。泛型擦除是指设计时定义泛型,在正常使用时再赋值,在运行时泛型被擦除:
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!
反协变、逆变:
- 父类型→子类型:越来越具体
specific
- 参数类型:要相反的变化,要不变或越来越抽象
class T {
void c( String s ) { … }
}
class S extends T {
@Override
void c( Object s ) { … }
}
但编译器报错。因为编译器认为这是 overload
而不是 overwirte
。
泛型中的 LSP
:
父类与子类的泛型必须是一模一样的:
List<String> x = new ArrayList<String>;
泛型擦除,Java
在执行时泛型全部代换,源程序:
public class Node<T> {
private T data;
private Node<T> next;
public Node(T data, Node<T> next) {
this.data = data;
this.next = next;
}
public T getData () { return data; }
// ...
}
执行时,有界直接擦除,无界直接用Object
代替:
public class Node {
private Object data;
private Node next;
public Node(Object data, Node next) {
this.data = data;
this.next = next;
}
public Object getData () {
return data; }
// ...
}
但通过通配符能让泛型即 <>
中不一致:
List<?>
使用通配符的情况:
- 不依赖
<>
类型,如size()
- 可能依赖
<>
,但操作是Object
方法,如equals()
或toString()
通配符也使用在:
<? super A>//下界为A,只能存A及其父类
<? extends A>//只能存A及其子类
判断某一个类是否是带通配符的类的子类(subtyping
关系),只需看是否符合集合包含关系即可:
List<Number> List<?>
List<Number> List<? extends Object>
List<Object> List<? super String>
2.委托和组合
委派/委托:一个对象请求另一个对象的功能。类/对象之间需要建立动态绑定。
- 隐式委托:由控制台等间接调用,编程中不知道谁实现、谁调用
- 显式委托:明确调用哪个对象的哪个方法
B
委托了 A
:
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");
}
}
A a = new A();
B b = new B(a); // establish delegation between two objects
比较因子:委托以实现比较大小的功能的类
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.weight())
return 1;
else if (.. == ..) return 0;
else return -1;
}
}
public void sort(List<Edge> edges) {
Comparator comparator = new EdgeComparator();
Collections.sort(edges, comparator);
}
另一种方法:让你的 ADT
实现 Comparable
接口,然后 override
compareTo()
方法。
与使用 Comparator
的区别:不需要构建新的 Comparator
类,比较代码放在 ADT
内部。但这种方法属于继承而不是委托。
public class Edge implements Comparable<Edge> {
Vertex s, t;
double weight;
...
public int compare To Edge o) {
if(this getWeight () > o.getWeight())
return 1;
else if (.. == ..) return 0;
else return -1;
}
}
继承与委托均是复用,
- 继承使用在有明确的父子关系时,是一种高效的复用方式
- 委托不需要类/对象之间存在语义联系
- “委托”发生在
object
层面,而“继承”发生在class
层面
CRP
原则,建议使用委托不用继承,其情况:
- 如果子类只需要复用父类中的一小部分方法。一个类不需要继承另一个类的全部方法,通过委托机制调用部分方法。
策略模式:遇到一种问题有多种解法的时候,可以根据环境或者条件的不同选择不同的算法或者策略来完成该功能。
规避了复杂的继承关系的委托实现方式:
- 使用接口定义系统必须对外展示的不同侧面的行为
- 接口之间通过
extends
实现行为的扩展(接口组合) - 类
implements
组合接口
委托:
Use
使用:临时性委托。只有当调用特定方法时才会产生两个类直接的关联。
class Duck {
//no field to keep Flyable object
void fly(Flyable f) {
f.fly();
}
}
Flyable f = new FlyWithWings();
Quackable q = new Quack();
Duck d = new Duck();
d.fly(f);
d.quack(q);
Association
关联:永久性委托。通过属性,在执行时用属性来调用方法。
class Duck {
Flyable f = new CannotFly();
void Duck(Flyable f) {
this.f = f;
}
void Duck() {
f = new FlyWithWings();//组合
}
void fly() {
f.fly();
}
}
Flyable f = new FlyWithWings();
Duck d = new Duck(f);
Duck d2 = new Duck();
d.fly();
- 组合(
Composition
):构造时建立不可改变的联系
class Duck {
Flyable f = new FlyWithWings();
void fly() {
f.fly();
}
}
Duck d = new Duck();
d.fly();
- 聚合(
Aggregation
):建立的是可以改变的联系
class Duck {
Flyable f;
void Duck(Flyable f) {
this.f = f;
}
void setFlyBehavior(f) {
this.f = f;
}
void fly() {
f.fly();
}
}
Flyable f = new FlyWithWings();
Duck d = new Duck(f);
d.fly();
d.setFlyBehavior(new CannotFly());
d.fly();
二. 设计系统级可复用库和框架
库的复用主动权在程序员,Framework
的复用主动权在 Framework
。
1. Framework
复用
- 白盒框架:把框架中核心方法放入父类实现,子类继承并对未实现的可定制方法进行扩展——继承
public abstract class PrintOnScreen {
public void print() {
JFrame frame = new JFrame();
JOptionPane.showMessageDialog(frame, textToShow());
frame.dispose();
}
protected abstract String textToShow();
}
public class MyApplication extends PrintOnScreen {
@Override
protected String textToShow() {
return "printing this text on " + "screen using PrintOnScreen " + "white Box Framework";
}
}
MyApplication m = new MyApplication();
m.print();
- 黑盒框架:用户可定制信息放入
API
,然后实现API
。暴露接口,然后用户自己实现。——委托
public class Application extends JFrame {
private JTextField textField;
private Plugin plugin;
public Application() { }
protected void init(Plugin p) {
p.setApplication(this);
this.plugin = p;
JPanel contentPane = new JPanel(new BorderLayout());
contentPane.setBorder(new BevelBorder(BevelBorder.LOWERED));
JButton button = new JButton();
button.setText(plugin != null ? plugin.getButtonText() : "ok");
contentPane.add(button, BorderLayout.EAST);
textField = new JTextField("");
if (plugin != null)
textField.setText(plugin.getInititalText());
textField.setPreferredSize(new Dimension(200, 20));
contentPane.add(textField, BorderLayout.WEST);
if (plugin != null)
button.addActionListener((e) -> { plugin.buttonClicked(); } );
this.setContentPane(contentPane);
...
}
public String getInput() { return textField.getText(); }
}
public interface Plugin {
String getApplicationTitle();
String getButtonText();
String getInititalText();
void buttonClicked();
void setApplication(Application app);
}
public final class PrintOnScreen {
TextToShow textToShow;
public PrintOnScreen(TextToShow tx)
this.textToShow = tx;
}
public void print() {
JFrame frame = new JFrame();
JOptionPane.showMessageDialog(frame, textToShow.text());
frame.dispose();
}
}
public interface TextToShow {
String text();
}
public class MyTextToShow implements TextToShow {
@Override
public String text() {
return "Printing";
}
}
PrintOnScreen m = new PrintOnScreen(new MyTextToShow());
m.print();
白盒框架用户实现的子类启动 main
,黑盒框架Framework
端启动 main
。