表妹:哥啊,我想喝奶茶。
我:走啊,去哪里喝?
表妹:走,我带你去,我经常去的那家,不但好喝,还可以自由搭配很多小料。我每次都是不同的搭配,换着喝,嘻嘻。
我:你倒是挺会喝的嘛~
你看,这不是很像我们设计模式中的装饰器模式嘛?
在我们生活中,还有很多这样的例子。比如,女孩子垫个鼻子,整个双眼皮;男孩子给自己的爱车改个刹车系统,改进气和排气系统等。
动态地给一个对象添加一些额外的职责,就增加功能来说,装饰器模式比生成子类更为灵活。
走,我们来去奶茶店看看。
基于继承的设计方案如果使用单纯使用继承的方式来实现奶茶的各种品类的话,结构体系如下图所示:
你看,如果新增一种奶茶,就要继承父类,新增一个实现子类,如果要新增一种新的搭配,比如,红豆珍珠果粒奶盖奶茶,那么就要继承已有的子类,也是要新增一个实现子类。那么,如此不断新增下去,就会导致继承体系过于庞大,且类膨胀。
那么,如果修改已有的子类呢,比如修改红豆奶盖,那么,红豆珍珠奶盖也要跟着修改。
所以,维护这样的业务系统是很难受的。这个时候,装饰器模式就派上用场啦~
基于装饰器模式的设计方案我们先来看看装饰器模式的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