Decorator装饰器设计模式

[软件构造] 05 Decorator装饰器设计模式

软件构造课程的4.3节——面向复用性的设计模式,对Decorator装饰器设计模式以及其他的5种设计模式进行了详细的讲述。并且lab3的实验指导书的第6种实现方案也提供了使用Decorator装饰器设计模式这种方案,但对于这种实现方案,我自认为是有一定的缺陷的。因此,本文将对Decorator装饰器设计模式进行详细的总结、分析和实现,最后详细地阐述使用lab3的第6种实现方案的缺陷,以及不推荐使用的原因。

1. 什么是装饰器设计模式? What?

GoF的《设计模式》这本书是这样描述的:
结构型模式之一。
允许动态地向一个现有的对象添加新的功能,同时又不改变其结构,相当于对现有的对象进行了一个包装。

2. 为什么要使用装饰器设计模式? Why?

试想下面这样一种需求。
假设我们想对栈(Stack)这种数据结构进行多种扩展:

1. UndoStack:允许撤销之前执行push或pop操作。
2. SecureStack: 执行push或pop操作需要输入密码。
3. SynchronizedStack: 能够序列化并发访问。
...

我们可以通过分别用一个继承自Stack类的子类来实现每一个不同的特性。

但是问题来了,如果我们需要这些特性的任意组合呢?例如需要同时满足1,2特性。
很自然的想法是设计一个继承树的层次结构,当需要增加的扩展很少时,这样做显然是可行的。但当扩展的数目不断增加时,继承树层次结构将出现阶乘式增长的现象,也就是发生了组合爆炸的现象。
由于Java不支持多继承,因而在继承树的实现过程中还将出现
大量的代码重复的问题。

出现上述问题的最重要的原因是:
使用继承来添加功能,是对整个类来进行操作的,而不是对某个对象进行操作的。继承是静态地给类添加功能,我们需要一种设计模式能够动态地给对象添加功能。于是装饰器模式应运而生,它利用委派的方式扩展对象行为,可以在运行时动态地进行扩展,写新的代码添加新功能,而无须修改现有代码。

3.装饰器设计模式结构分析

Decorator装饰器设计模式_第1张图片

装饰器设计模式的一般结构有四种角色:抽象组件、具体组件、抽象装饰器、具体装饰器。

1.Component(抽象组件)
抽象组件定义了对象的接口,可以动态地对接口中方法进行装饰。
2.ConcreteComponent(具体组件)
被装饰者,继承自抽象组件,实现自己原有的方法,在原有方法的基础上增加功能(装饰)。
3.Decorator(抽象装饰类)
继承自抽象组件,持有一个抽象组件类型的引用,实际可接收一个具体组件或另一个装饰器(是能够实现动态装饰的关键,通过委托将原有的功能交由它来实现,而在此基础上添加新的功能)。
4.ConcreteDecorator(具体装饰类)
负责实现具体的装饰功能,发挥实际的装饰作用,派生出的不同的具体装饰器可以产生不同的装饰效果。

4. 一个具体的实现 How?

//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();
    }

}
//输出结果:
/*
陆地上跑!
天上飞!
水里游!
*/

5. 装饰器设计模式的适用情况 When?

在不影响其他对象的情况下,以动态、透明的方式给某个对象的方法添加新功能。

接口的一致性
需要注意的是装饰器是以增强原接口功能的方式(保持接口不变,在原接口中添加新的功能)来扩展类的功能。因而进行多层装饰时,装饰器类需要跟原始类继承相同的接口。

6. 优缺点

优点:

  • 比继承更灵活
  • 可以通过动态的方式来扩展一个对象的功能,在运行时选择不同的装饰器,从而实现不同的行为。

缺点:

  • 会产生很多的小对象,增加复杂性
  • 多层装饰的对象,调试时寻找错误可能需要逐级排查,较为烦琐。

7. lab3实验的第6种实现方案

Decorator装饰器设计模式_第2张图片

这是六种方案中的最后一种,我原以为可能是最好的一种设计方案,因为它最复杂,而且是最后一个(一般最好的都放在最后),但经过我对这种方案的认真分析,我认为这样做是有缺陷的。

下面以其中一个维度为例讲述我对此方案的看法:
Decorator装饰器设计模式_第3张图片

该方案最大的问题在参数的传递上,以位置维度为例,对于不同的位置数量,我们正常的想法是需要三种不同的情况分别设置一个接口,放置到Component(抽象组件)中,然后对其进行不同的装饰,这样做的结果使得五个维度层层装饰下来,每一层中的接口都过于庞大和臃肿。

然后我的想法是能不能以一种统一的方式来进行参数的传递,例如使用参数列表的方式,这样的方式确实能做,但也就根本不用进行装饰了,放到oncreteComponent(具体组件)中进行实现就好了。

可是对于其他的维度,有的并不能以一种统一的方式来进行参数的传递,必然会导致接口中出现多余的重载方法,因而在装饰时需要对多余的重载方法禁用(例如,override 后的方法体设置为空实现,或者直接 assert false,或者 throws new Exception)。但是,这么做会导致各个应用子类中大量 override 空实现的方法,代码会很不美。另一方面,这种做法会导致违反LSP原则,因为子类中空实现的方法不再符合接口中方法的spec,装饰后的子类型对象无法替代父类型对象。

你可能感兴趣的:(软件构造)