和表妹去喝奶茶

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

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


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

基于继承的设计方案

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

设计模式之【装饰器模式】-LMLPHP

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

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

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

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

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

设计模式之【装饰器模式】-LMLPHP

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

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

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

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

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

设计模式之【装饰器模式】-LMLPHP

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

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

03-16 22:55