• 设计模式之【装饰器模式】


    和表妹去喝奶茶

    表妹:哥啊,我想喝奶茶。

    :走啊,去哪里喝?

    表妹:走,我带你去,我经常去的那家,不但好喝,还可以自由搭配很多小料。我每次都是不同的搭配,换着喝,嘻嘻。

    :你倒是挺会喝的嘛~

    你看,这不是很像我们设计模式中的装饰器模式嘛?

    在我们生活中,还有很多这样的例子。比如,女孩子垫个鼻子,整个双眼皮;男孩子给自己的爱车改个刹车系统,改进气和排气系统等。


    动态地给一个对象添加一些额外的职责,就增加功能来说,装饰器模式比生成子类更为灵活。

    走,我们来去奶茶店看看。

    基于继承的设计方案

    如果使用单纯使用继承的方式来实现奶茶的各种品类的话,结构体系如下图所示:

     

    你看,如果新增一种奶茶,就要继承父类,新增一个实现子类,如果要新增一种新的搭配,比如,红豆珍珠果粒奶盖奶茶,那么就要继承已有的子类,也是要新增一个实现子类。那么,如此不断新增下去,就会导致继承体系过于庞大,且类膨胀。

    那么,如果修改已有的子类呢,比如修改红豆奶盖,那么,红豆珍珠奶盖也要跟着修改。

    所以,维护这样的业务系统是很难受的。这个时候,装饰器模式就派上用场啦~

    基于装饰器模式的设计方案

    我们先来看看装饰器模式的UML类图:

     

    • Component:组件对象的抽象接口,可以给这些对象动态的增加职责/功能。

    • ConcreteComponent:具体的组件的对象,实现组件对象的接口,是被装饰器装饰的原始对象,即可以给这个对象动态地添加职责。

    • Decorator:所有装饰器的抽象父类,实现了组件对象的接口,并且持有一个组件对象(被装饰的对象)。

    • ConcreteDecorator:具体的装饰器,具体实现向装饰对象添加功能。

    现在,奶茶店使用装饰器模式后,如下图所示:

     

    我们之前在学习桥接模式的时候,说过【组合优于继承】。你看,现在是不是清晰很多了,如果奶茶小料有新增的话,直接实现一个小料的装饰器即可,客户想怎么搭配都可以。接下来,我们看看具体的代码实现。

    Component:奶茶抽象类,该类定义了制作奶茶的抽象方法。

    复制代码
    1 // 奶茶
    2 abstract class MilkTea {
    3 4     // 制作奶茶的过程
    5     public abstract void process();
    6 7 }
    复制代码

    ConcreteComponent:奶茶店提供的最基本的招牌奶茶,这种奶茶不加任何小料。

    复制代码
     1 // 招牌奶茶
     2 class SignatureMilkTea extends MilkTea {
     3  4     @Override
     5     // 制作招牌奶茶的过程
     6     public void process() {
     7         System.out.println("招牌奶茶制作完成");
     8     }
     9 10 }
    复制代码

    另外2种奶茶:红豆奶茶和珍珠奶茶,不是用继承实现,而是用装饰器实现。这两种奶茶都是基于上面招牌奶茶添加不同的小料制作而成的。Decorator:为了方便扩展,下面实现一个装饰器的抽象类。

    复制代码
     1 // 奶茶装饰器
     2 abstract class MilkTeaDecorator extends MilkTea {
     3  4     private MilkTea milkTea;
     5  6     public MilkTeaDecorator(MilkTea milkTea) {
     7         this.milkTea = milkTea;
     8     }
     9 10     @Override
    11     public void process() {
    12         this.milkTea.process();
    13     }
    14 }
    复制代码

    ConcreteDecorator:接下来,就根据这个奶茶装饰器,实现红豆奶茶和珍珠奶茶。

    复制代码
     1 // 红豆奶茶
     2 class RedBeanMilkTea extends MilkTeaDecorator {
     3  4     public RedBeanMilkTea(MilkTea milkTea) {
     5         super(milkTea);
     6     }
     7  8     @Override
     9     public void process() {
    10         super.process();
    11         System.out.println("加点红豆");
    12     }
    13 }
    14 15 // 珍珠奶茶
    16 class BubbleTea extends MilkTeaDecorator {
    17 18     public BubbleTea(MilkTea milkTea) {
    19         super(milkTea);
    20     }
    21 22     @Override
    23     public void process() {
    24         super.process();
    25         System.out.println("加点珍珠");
    26     }
    27 }
    复制代码

    昨天,表妹喝了红豆奶茶:

    复制代码
    1 MilkTea milkTea = new SignatureMilkTea();
    2 RedBeanMilkTea redBeanMilkTea = new RedBeanMilkTea(milkTea);
    3 redBeanMilkTea.process();
    4 5 // 打印结果为:
    6 // 招牌奶茶制作完成
    7 // 加点红豆
    复制代码

    今天,她说她想和红豆珍珠奶茶:

    复制代码
    1 MilkTea milkTea = new SignatureMilkTea();
    2 RedBeanMilkTea redBeanMilkTea = new RedBeanMilkTea(milkTea);
    3 BubbleTea redBeanBubble = new BubbleTea(redBeanMilkTea);
    4 redBeanBubble.process();
    5 6 // 打印结果为:
    7 // 招牌奶茶制作完成
    8 // 加点红豆
    9 // 加点珍珠
    复制代码

    你看,是不是很容易就满足了客户的需求。那如果还可以加奶盖,应该怎么设计呢?

    复制代码
     1 // 奶盖奶茶
     2 class MilkCapMilkTea extends MilkTeaDecorator {
     3  4     public MilkCapMilkTea(MilkTea milkTea) {
     5         super(milkTea);
     6     }
     7  8     @Override
     9     public void process() {
    10         super.process();
    11         System.out.println("加上奶盖");
    12     }
    13 }
    复制代码

    你看,直接继承奶茶装饰器MilkTeaDecorator,实现一个MilkCapMilkTea类即可,然后客户想怎么搭配都可以:

    复制代码
     1 // 奶盖招牌奶茶
     2 MilkTea milkTea = new SignatureMilkTea();
     3 MilkCapMilkTea milkCapMilkTea = new MilkCapMilkTea(milkTea);
     4 milkCapMilkTea.process();
     5  6 // 加了红豆、珍珠和奶盖的奶茶
     7 MilkTea milkTea = new SignatureMilkTea();
     8 MilkCapMilkTea milkCapMilkTea = new MilkCapMilkTea(milkTea);
     9 RedBeanMilkTea redBeanMilkTea = new RedBeanMilkTea(milkCapMilkTea);
    10 BubbleTea luxuryMilkTea = new BubbleTea(redBeanMilkTea);
    11 luxuryMilkTea.process();
    复制代码

    你看,是不是扩展很灵活,遵守了开-闭原则。而且,如果已经有小料装饰器的话,不管客户如何搭配,我们都不需要增加实现类,从而避免了类膨胀的问题。

    如果使用继承来实现该业务系统,就会导致继承体系过于庞大,类膨胀等问题,如果有一天要修改或删除某一个子类,就会导致牵一发而动全身,这是非常可怕的。而装饰器模式提供了一个非常好的解决方案,它把每个要装饰的“奶茶小料”放在单独的类中,并让这个类包装它所要装饰的对象(奶茶),因此,当需要添加小料的时候,客户代码就可以在运行时候根据需要有选择地、按顺序地使用装饰功能包装对象了。

    可能有些同学会问,这不就是代理模式嘛?

    装饰器模式和代理模式的区别

    是的,对于装饰器模式来说,装饰者和被装饰者都实现一个接口;对代理模式来说,代理类和委托类也都实现同一个接口。不论我们使用哪一种模式,都可以很容易地在真实对象的方法前面或后面加上自定义的方法。

    这里我们先简单看一下两者的区别,另外一篇我们再仔细分析这两种设计模式的区别哈。

    代理模式注重的是对对象的某一功能的流程把控和辅助,它可以控制对象做某些事,重点是为了借用对象的功能完成某一流程,而非对象功能如何。

    装饰器模式注重的是对对象功能的扩展,不关心外界如何调用,只注重对对象功能加强,装饰后还是对象本身。

    装饰器模式的优点

    • 比继承更灵活

      从为对象添加功能的角度来看,装饰器模式比继承更灵活。继承是静态的,而且一旦继承所有子类都有一样的功能。而装饰器模式采用把功能分离到每个装饰器当中,然后通过对象组合的方式,在运行时动态地组合功能,每个被装饰的对象最终有哪些功能,是由运行期动态组合的功能来决定的

    • 更容易复用功能

      装饰器模式把一系列复杂的功能分散到每个装饰器当中,一般一个装饰器只实现一个功能,使实现装饰器变得简单,更重要的是这样有利于装饰器功能的复用,可以给一个对象增加多个同样的装饰器,也可以把一个装饰器用来装饰不同的对象,从而实现复用装饰器的功能。

    • 简化高层定义

      装饰器模式可以通过组合装饰器的方式,为对象增添任意多的功能。因此在进行高层定义的时候,不用把所有的功能都定义出来,而是定义最基本的就可以了,可以在需要使用的时候,组合相应的装饰器来完成所需的功能。

    装饰器模式的缺点

    • 会产生很多细粒度对象。

      前面说了,装饰器模式是把一系列复杂的功能,分散到每个装饰器当中,一般一个装饰器只实现一个功能,这样会产生很多细粒度的对象,而且功能越复杂,需要的细粒度对象越多。

    • 多层的装饰是比较复杂的。

      就像剥洋葱一样,你剥到了最后才发现是最里层的装饰出现了问题,这个工作量是很大的。因此,尽量减少装饰类嵌套的层数,以便降低系统的复杂度。

    装饰器模式的应用场景

    • 需要扩展一个类的功能,或给一个类增加附加功能;

    • 需要动态地给一个对象增加功能,这些功能可以再动态地撤销;

    • 需要为一批的兄弟类进行改装或加装功能。

    总结

    继承是静态地给类添加功能,而装饰器模式则是动态地增加功能。

    Java IO类中大量使用了装饰器模式,学完该模式,可以去看看源码中是如何使用的,巩固知识。

    参考

    《研磨设计模式》

    《设计模式之禅》

    https://mp.weixin.qq.com/s/BquPNZmG3tvG562hJm3YsQ

  • 相关阅读:
    创业 4 年获近 7000 万美元融资,53 岁老程序员 all in 开源
    普冉PY32系列(十五) PY32F0系列的低功耗模式
    2022年起重机械指挥考试题及模拟考试
    【 OpenGauss源码学习 —— (hash_search)】
    了解操作系统,什么是操作系统Operation System?
    linux系统下操作I2C总线外设(imx6ull的oled显示屏i2c驱动笔记)
    const和readonly的区别
    计算机基础学习(好文必看)
    git撤销操作(git后悔药)
    使用element的过渡效果来做动效
  • 原文地址:https://www.cnblogs.com/Gopher-Wei/p/16014714.html