装饰模式:如何在基础组件上扩展新功能?

243 阅读7分钟

在前面几篇中,我们已经了解了结构型模式中的适配器模式、桥接模式和组合模式。本篇我们要介绍的装饰模式看上去和适配器模式、桥接模式很相似,都是使用组合方式来扩展原有类的,但其实本质上却相差甚远呢。

简单来说,适配器模式侧重于转换,而装饰模式侧重于动态扩展;桥接模式侧重于横向宽度的扩展,而装饰模式侧重于纵向深度的扩展。那么装饰模式到底有哪些需要我们重点学习的地方呢?下面,我们一起来看看吧

一、模式原理分析

装饰模式的原始定义是:允许动态地向一个现有的对象添加新的功能,同时又不改变其结构,相当于对现有的对象进行了一个包装。

这个定义非常清晰易懂,因为不能直接修改原有对象的功能,只能在外层进行功能的添加,所以装饰模式又叫包装器模式

下面我们还是直接来看看装饰模式的 UML 图:

从 UML 图中,我们能发现装饰模式的四个关键角色。

  • 组件:作为装饰器类包装的目标类。

  • 具体组件:实现组件的基础子类。

  • 装饰器:一个抽象类,其中包含对组件的引用,并且还重写了组件接口方法。

  • 具体装饰器:继承扩展了装饰器,并重写组件接口方法,同时可以添加附加功能。

接下来我们再来看看它的代码实现:

//组件
public interface Component {
    void excute();
} 
//具体组件
public class BaseComponent implements Component {
    @Override
    public void excute() {
        //do something
    }
}
//装饰器
public class BaseDecorator implements Component {
    private Component wrapper;
    public BaseDecorator(Component wrapper) {
        this.wrapper = wrapper;
    }
    @Override
    public void excute() {
        wrapper.excute();
    }
}
//具体装饰器A
public class DecoratorA extends  BaseDecorator {
    public DecoratorA(Component wrapper) {
        super(wrapper);
    }
    @Override
    public void excute() {
        super.excute();
    }
}
//具体装饰器B
public class DecoratorB extends  BaseDecorator {
    public DecoratorB(Component wrapper) {
        super(wrapper);
    }
    @Override
    public void excute() {
        super.excute();
    }
}

这段代码实现比较简单,组件 Component 定义了组件具备的基本功能,具体组件 BaseComponent 是对组件(接口)的一种基础功能的实现,装饰器 BaseDecorator 中包含 Component 的抽象实例对象,作为装饰器装饰的目标对象,具体装饰器 DecoratorA 和 DecoratorB 继承装饰器 BaseDecorator 来进行具体附加功能的沿用与扩展。

所以说,装饰模式本质上就是给已有不可修改的类附加新的功能,同时还能很方便地撤销

二、使用场景分析

一般来讲,装饰模式常用的使用场景有以下几种。

  • 快速动态扩展和撤销一个类的功能场景。 比如,有的场景下对 API 接口的安全性要求较高,那么就可以使用装饰模式对传输的字符串数据进行压缩或加密。如果安全性要求不高,则可以不使用。

  • 可以通过顺序组合包装的方式来附加扩张功能的场景。 比如,加解密的装饰器外层可以包装压缩解压缩的装饰器,而压缩解压缩装饰器外层又可以包装特殊字符的筛选过滤的装饰器等。

  • 不支持继承扩展类的场景。 比如,使用 final 关键字的类,或者系统中存在大量通过继承产生的子类。

在现实中有一个很形象的关于装饰器使用场景的例子,那就是单反相机镜头前的滤镜。用过单反相机的同学应该知道,不加滤镜其实不会影响拍照,而滤镜实际上就是一个装饰器,滤镜上又可以加滤镜,这样就做到了不改变镜头而又给镜头增加了附加功能。

三、为什么要使用装饰器模式

分析完装饰模式的原理和使用场景后,我们再来说说使用装饰模式的原因,主要有以下两个。

第一个,为了快速动态扩展类功能,降低开发的时间成本。 比如,一个类 A,有子类 A01、A02,然后 A01 又有子类 A001,以此类推,A0001、A00001……这样的设计会带来一个严重的问题,那就是:当需要扩展 A01 时,所有 A01 的子类和父类都会受到影响。但是,如果这时我们使用装饰器 B01、B02、C01、C02,那么扩展 A01 就会变为 A01B01C01、A01B02C02 这样的组合。这样就能快速地扩展类功能,同时还可以按需来任意组合,极大地节省了开发时间。

第二个,希望通过继承的方式扩展老旧功能。 比如,前面我们说到,当类标识有 final 关键字时,要想复用这个类就只能通过重新复制代码的方式,不过通常这样的类又处于需要对外提供功能的状态,不能轻易修改,而梳理上下文逻辑又费时费力,那么采用装饰模式就是一个很好的选择。因为装饰器是在外层进行扩展,即使功能不合适,也能及时地撤销而不影响原有的功能。所以说,在一些维护系统的升级或重构场景中,使用装饰模式来重构代码,在短期内都能达到快速解耦的效果。

四、装饰器模式的优缺点是什么?

使用装饰模式主要有以下四个大的优点。

  • 快速扩展对象的功能。 对于一些独立且无法修改的类来说,当需要在短期内扩展功能时,采用装饰模式能快速有效地扩展功能,同时也不会影响原有的功能。

  • 可以动态增删对象实例的功能。 比如,在上面文件读写器的例子中,我们可以在创建对象的时候再决定是一起使用压缩装饰器和加密装饰器,还是分开使用,或者只是用基本的读写功能。

  • 可以在统一行为上组合几种行为。 装饰模式是对某一个接口行为进行的组合扩展,通过包装的方式不断扩展代码的行为,从而实现了更多行为的组合。

  • 满足单一职责原则。 每一个具体装饰器类只实现一个组件的具体行为,即便附加了新的功能也是围绕着组件的职责而做扩展,保证了职责的单一性。

同样,装饰模式也有一些缺点。

  • 在调用链中删除某个装饰器时需要修改代码。 装饰模式的最大弊端在于,当在某个组件上附加了太多装饰器后,想要删除其中的某个装饰器时,就需要修改前后的装饰器的引用位置,这样容易导致上下文中代码都需要修改的情况,大大增加了出错的可能性。

  • 容易导致产生很多装饰对象,增加代码理解难度。 由于使用了组合方式,并且在调用时使用了链式结构,这样间接增加了很多装饰器对象,而一旦不了解装饰模式的特性,就很容易误解为多个对象的参数调用,增加了代码的理解难度。

  • 增加问题定位和后期代码维护成本。 虽然装饰模式使用的组合方式比继承更加灵活,但同时也会增加代码的复杂性,在维护代码时会增加问题定位难度,同时调试时也需要逐级排查,比较烦琐,增加了后期代码维护成本。

装饰模式就像是我们送人礼物时的“包装盒”,我们可以选择各种各样的包装盒,还可以在包装盒里嵌套包装盒。

装饰模式在结构上体现为链式结构,通过在外层不断地添加具体装饰器类来对原有的组件类进行扩展,这样在保证原有功能的情况下,还能额外附加新的功能。这也是学习和理解装饰模式的核心所在。

文章(专栏)将持续更新,欢迎关注公众号:服务端技术精选。欢迎点赞、关注、转发