软件构造 4-2 Construction for Reuse

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

一. 设计可复用的类

  • 继承与重写
  • 重载
  • 参数多态与泛型编程
  • 行为子类型与 Liskov 替换原则
  • 组合与委托

1.1 LSP 原则

  subtying :若 B 继承 A ,则 BA 的子类型,即任意 B 均为 AA 能做的事,B 也要能做。又称为行为 subtyping

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

  在可以使用 a 的场景,都可以用 c1c2 代替而不会有任何问题:

a = c1;
a = c2;

  子类型多态:客户端可用统一的方式处理不同类型的对象。

  检查 BA 的子类型的合理性(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 需要比较大小,或者要放入 CollectionsArrays 进行排序,可实现 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();
  1. 组合(Composition):构造时建立不可改变的联系
class Duck {
	Flyable f = new FlyWithWings();
	void fly() {
		f.fly();
	}
}

Duck d = new Duck();
d.fly();
  1. 聚合(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

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值