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