软件构造课程的4.3节——面向复用性的设计模式,对Decorator装饰器设计模式以及其他的5种设计模式进行了详细的讲述。并且lab3的实验指导书的第6种实现方案也提供了使用Decorator装饰器设计模式这种方案,但对于这种实现方案,我自认为是有一定的缺陷的。因此,本文将对Decorator装饰器设计模式进行详细的总结、分析和实现,最后详细地阐述使用lab3的第6种实现方案的缺陷,以及不推荐使用的原因。
GoF的《设计模式》这本书是这样描述的:
结构型模式之一。
允许动态地向一个现有的对象添加新的功能,同时又不改变其结构,相当于对现有的对象进行了一个包装。
试想下面这样一种需求。
假设我们想对栈(Stack)这种数据结构进行多种扩展:
1. UndoStack:允许撤销之前执行push或pop操作。
2. SecureStack: 执行push或pop操作需要输入密码。
3. SynchronizedStack: 能够序列化并发访问。
...
我们可以通过分别用一个继承自Stack类的子类来实现每一个不同的特性。
但是问题来了,如果我们需要这些特性的任意组合呢?例如需要同时满足1,2特性。
很自然的想法是设计一个继承树的层次结构,当需要增加的扩展很少时,这样做显然是可行的。但当扩展的数目不断增加时,继承树层次结构将出现阶乘式增长的现象,也就是发生了组合爆炸的现象。
由于Java不支持多继承,因而在继承树的实现过程中还将出现
大量的代码重复的问题。
出现上述问题的最重要的原因是:
使用继承来添加功能,是对整个类来进行操作的,而不是对某个对象进行操作的。继承是静态地给类添加功能,我们需要一种设计模式能够动态地给对象添加功能。于是装饰器模式应运而生,它利用委派的方式扩展对象行为,可以在运行时动态地进行扩展,写新的代码添加新功能,而无须修改现有代码。
装饰器设计模式的一般结构有四种角色:抽象组件、具体组件、抽象装饰器、具体装饰器。
1.Component(抽象组件)
抽象组件定义了对象的接口,可以动态地对接口中方法进行装饰。
2.ConcreteComponent(具体组件)
被装饰者,继承自抽象组件,实现自己原有的方法,在原有方法的基础上增加功能(装饰)。
3.Decorator(抽象装饰类)
继承自抽象组件,持有一个抽象组件类型的引用,实际可接收一个具体组件或另一个装饰器(是能够实现动态装饰的关键,通过委托将原有的功能交由它来实现,而在此基础上添加新的功能)。
4.ConcreteDecorator(具体装饰类)
负责实现具体的装饰功能,发挥实际的装饰作用,派生出的不同的具体装饰器可以产生不同的装饰效果。
//Component(抽象组件)
public interface ICar{
void move();
}
//ConcreteComponent(具体组件)
class Car implements ICar{
@Override
public void move() {
System.out.println("陆地上跑!");
}
}
//Decorator(抽象装饰类)
abstract class SuperCar implements ICar{
protected ICar car;
public SuperCar(ICar car) {
super();
this.car = car;
}
@Override
public void move() {
car.move();
}
}
//ConcreteDecorator(具体装饰类)
//三个具体装饰器
class FlyCar extends SuperCar{
public FlyCar(ICar car) {
super(car);
}
public void fly() {
System.out.println("天上飞!");
}
@Override
public void move() {
super.move();
fly();
}
}
class WaterCar extends SuperCar{
public WaterCar(ICar car) {
super(car);
}
public void swim() {
System.out.println("水里游!");
}
@Override
public void move() {
super.move();
swim();
}
}
class AICar extends SuperCar{
public AICar(ICar car) {
super(car);
}
public void autoMove() {
System.out.println("自动驾驶!");
}
@Override
public void move() {
super.move();
autoMove();
}
}
//客户端调用
public class Client {
public static void main(String[] args) {
ICar car=new Car();
ICar flyCar=new FlyCar(car);
ICar waterflyCar=new WaterCar(flyCar);
waterflyCar.move();
}
}
//输出结果:
/*
陆地上跑!
天上飞!
水里游!
*/
在不影响其他对象的情况下,以动态、透明的方式给某个对象的方法添加新功能。
接口的一致性
需要注意的是装饰器是以增强原接口功能的方式(保持接口不变,在原接口中添加新的功能)来扩展类的功能。因而进行多层装饰时,装饰器类需要跟原始类继承相同的接口。
优点:
缺点:
这是六种方案中的最后一种,我原以为可能是最好的一种设计方案,因为它最复杂,而且是最后一个(一般最好的都放在最后),但经过我对这种方案的认真分析,我认为这样做是有缺陷的。
该方案最大的问题在参数的传递上,以位置维度为例,对于不同的位置数量,我们正常的想法是需要三种不同的情况分别设置一个接口,放置到Component(抽象组件)中,然后对其进行不同的装饰,这样做的结果使得五个维度层层装饰下来,每一层中的接口都过于庞大和臃肿。
然后我的想法是能不能以一种统一的方式来进行参数的传递,例如使用参数列表的方式,这样的方式确实能做,但也就根本不用进行装饰了,放到oncreteComponent(具体组件)中进行实现就好了。
可是对于其他的维度,有的并不能以一种统一的方式来进行参数的传递,必然会导致接口中出现多余的重载方法,因而在装饰时需要对多余的重载方法禁用(例如,override 后的方法体设置为空实现,或者直接 assert false,或者 throws new Exception)。但是,这么做会导致各个应用子类中大量 override 空实现的方法,代码会很不美。另一方面,这种做法会导致违反LSP原则,因为子类中空实现的方法不再符合接口中方法的spec,装饰后的子类型对象无法替代父类型对象。