寂然解读设计模式 - 装饰者模式

I walk very slowly, but I never walk backwards 

设计模式 - 装饰者模式


寂然

大家好,我是寂然,本节课,我们来聊设计模式中的装饰者模式,当然,首先,来一杯塞纳河畔,左岸的咖啡☕️

案例需求 - 星巴克咖啡

来看一个星巴克咖啡订单项目需求:

  • 咖啡种类/单品咖啡:Espresso(意大利浓咖啡)、Cappuccino(卡布奇诺)、Cafe Latte(冰拿铁)

  • 配料:Milk、sugar

客户可以点单品咖啡,也可以单品咖啡+配料,根据客户订单计算不同种类咖啡的费用

要求在扩展新的咖啡种类时,具有良好的扩展性、改动方便、维护方便

解决方案一:一般实现

针对上面的需求,我们一般想到的实现方式是定义一个基类Coffee,然后让各种单品咖啡去继承基类 Coffee,重写里面的description() 和 cost() 方法,当然咖啡里还可以加东西,同样我们可以使用这种方式,就是把咖啡和每一种配料,进行组合,类图如下:


寂然解读设计模式 - 装饰者模式


其实这种思路我们不需要去实现,大家很容易就会发现,这样去做,整个项目的可维护性和可扩展性非常差,如果咖啡店新增加了一种单品咖啡,数量就会倍增,因为咖啡里还可以加东西,那就会出现类爆炸问题,所以这种方法可以实现业务逻辑,但是考虑到扩展和维护起来太差,不符合需求,所以不可取

解决方案二:配料改进

OK,前面我们分析,使用方案一解决咖啡订单项目,由于客户可以点单品咖啡加任意配料,所以使用方案一会造成类的倍增,扩展性和可维护性都非常差,因此我们需要进行改进,那有的小伙伴说了,我们可以把配料内置到Coffee 类中,这样就不会造成类的数量倍增了,我们来看下简易类图


寂然解读设计模式 - 装饰者模式


方案二我们把配料内置到Coffee 类中,那各种单品咖啡重只需要继承Coffee类即可,可以根据返回值来确定是否要添加各种类型的配料,例如 hasSugar() 方法返回int类型,那某一个单品咖啡重写该方法,返回 0 表示不加糖,返回1或者其他表示加的糖的份数,然后cost() 方法完成计费即可,这样就不会造成类的数量倍增,新增单品咖啡添加一个类即可,项目的可维护性提高了

方案分析

但其实方案二也存在一些问题,虽然方案二控制了类的数量,不至于造成类爆炸,但是针对配料而言,按照方案二的思路,每一种配料都需要提供has() 方法和 set() 方法,考虑到实际上咖啡中可以加的配料有很多,那在对配料种类进行维护(CRUD)的时候,代码量还是很大,所以虽然方案二针对扩展性和维护性较方案一而言,有了很大的提升,但是也不是咖啡订单项目的最优解,那铺垫了这么久,这里就可以引出我们的主角 - 装饰者模式了

基本介绍

装饰者模式:在不改变原有对象的基础之上,动态的将功能附加到对象上,提供了比继承更有弹性的替代方案(扩展原有对象功能) ,装饰者模式也体现了开闭原则(ocp)

这里提到的动态的将新功能附加到对象和 ocp 原则,在后面的应用实例上会以代码的形式体现

原理类图

其实上面的概念比较抽象,我们换一种思路,结合装饰者模式的原理类图,我们来理解装饰者模式


寂然解读设计模式 - 装饰者模式


装饰者模式就类比于大家打包一个快递,比如我们要给朋友打包邮寄一个笔记本电脑,肯定不能直接邮寄,需要装在纸箱里,并且外面包裹快递袋,其实这里的笔记本电脑就是主体 - Component,也就是装饰者模式中的被装饰者,而纸箱以及快递袋就是包装 - Decorator 即装饰者,所以根据类图,我们可以抽象出装饰者模式的一些角色

装饰者模式角色

  • Component 主体:定义一个主体的模板,类比前面星巴克项目的基类 Coffee

  • ConcreteComponent:具体的主体, 类比前面的各类单品咖啡

  • Decorator:装饰者,类比咖啡中的各种配料,(根据类图的思路可以看到,装饰者里面聚合了主体即被装饰者是一种反向的思维,后面代码中大家就能体会到这样设计的好处)

  • ConcreteDecoratorA /B:具体的装饰角色,负责具体的装饰细节

当然,在如图的 Component 与 ConcreteComponent 之间,如果 ConcreteComponent 类很多,还可以设计一个缓冲层,将共有的部分提取出来,再抽象出一层

解决方案三:装饰者模式

类图展示

寂然解读设计模式 - 装饰者模式


抽象基类Coffee,就是装饰者模式角色中主体

Espresso 等就是具体的单品咖啡,即ConcreteComponent

Decorator 是装饰者,聚合了被装饰者 Coffee

Decorator 的cost() 方法会采用递归的方式,进行费用的叠加计算

装饰者模式下订单思路

为何需要递归呢?假设现在客户下了一单咖啡,点了卡布奇诺加一份 milk 加两份 sugar ,那其实是这样的思路


寂然解读设计模式 - 装饰者模式


1)里层,Milk包含了 Cappuccino ,sugar包含了 Milk + Cappuccino

2)再加一份糖,就是 sugar包含了 sugar + Milk + Cappuccino

3)这样不管是什么形式的单品咖啡加配料,通过递归方式都可以方便的组合和维护

代码演示
//主体/被装饰者
public abstract class Coffee {
​
 private String desc; //描述
​
 private float price = 0.0f;
​
 public String getDesc() {
 return desc;
 }
​
 public void setDesc(String desc) {
 this.desc = desc;
 }
​
 public float getPrice() {
 return price;
 }
​
 public void setPrice(float price) {
 this.price = price;
 }
​
 //计算费用的抽象方法,子类来实现
 public abstract float cost();
​
}
​
//卡布奇诺
public class Cappuccino extends Coffee {
​
 public Cappuccino(){
​
 setDesc("卡布奇诺");
​
 setPrice(24.0f);
 }
​
 @Override
 public float cost() {
 return super.getPrice();
 }
}
​
//意大利浓咖啡
public class Espresso extends Coffee{
​
​
 public Espresso(){
​
 setDesc("意大利浓咖啡");
​
 setPrice(18.0f);
 }
​
 @Override
 public float cost() {
 return super.getPrice(); //对于单品咖啡而言
 }
}
​
//装饰者
public class Decorator extends Coffee {
​
 //聚合被装饰者
 private Coffee coffee;
​
 public Decorator(Coffee coffee){
​
 this.coffee = coffee;
 }
​
 //重写计费方法
 @Override
 public float cost() {
 return super.getPrice() + coffee.cost();
 }
​
 @Override
 public String getDesc() {
 return super.getDesc() + coffee.getDesc();
 }
}
​
//牛奶
public class Milk extends Decorator{
​
 public Milk(Coffee coffee) {
​
 super(coffee);
​
 setDesc("牛奶");
​
 setPrice(3.0f);
 }
}
​
//糖
public class Sugar extends Decorator {
​
 public Sugar(Coffee coffee) {
​
 super(coffee);
​
 setDesc("方糖");
​
 setPrice(2.0f);
 }
}
​
//咖啡店(客户端)
public class CoffeeStore {
​
 public static void main(String[] args) {
​
 //点了卡布奇诺加一份 milk 加两份 sugar
​
 //先有卡布奇诺
 Coffee order = new Cappuccino(); //订单还没结束
​
//        System.out.println(order.cost());
​
//        System.out.println(order.getDesc());
​
 //加入一份牛奶 直接放进去, 使用装饰者模式
 order = new Milk(order);
​
//        System.out.println(order.cost());
//
//        System.out.println(order.getDesc());
​
 //加入一份糖
 order = new Sugar(order);
​
//        System.out.println(order.cost());
​
//        System.out.println(order.getDesc());
​
 //再加入一份糖
 order = new Sugar(order);
​
 System.out.println(order.cost());
​
 System.out.println(order.getDesc());
​
 }
}
设计优势

在当前模式下,如果我们要新增一种单品咖啡,你会发现,继承 Coffee类就可以用了,同样,新增一种配料也是如此,那这样设计的扩展性是非常优秀的,而且非常灵活,我可以随意单点或者加各种配料,组合多少都无所谓,是完成星巴克咖啡订单比较优质的解法之一

装饰者模式VS继承
  • 装饰者模式与继承关系的目的都是要扩展对象的功能,但是装饰者模式可以提供比继承更多的灵活性,装饰者模式允许系统动态决定“贴上”一个需要的“装饰”,或者除掉一个不需要的“装饰,继承关系则不同,继承关系是静态的,它在系统运行前就决定了,

  • 如果采用装饰者模式,相对继承而言,需要类的数目就会大大减少 ,因为如果都是用继承的方法实现的,那么每一种组合都需要一个类,就会造成大量性能重复的类出现,当然, 在另一方面,使用装饰模式会产生比使用继承关系更多的对象

JDK - IO源码解析

装饰者模式在Java语言中的最著名的应用莫过于Java I/O标准库的设计了,下面,我们一起来看下装饰者模式在IO源码里的应用,由于Java I/O库需要很多性能的各种组合,如果这些性能都是用继承的方法实现的,那么每一种组合都需要一个类,这样就会造成大量性能重复的类出现,而如果采用装饰者模式,那么类的数目就会大大减少,性能的重复也可以减至最少,因此装饰者模式是Java I/O库的基本模式

在Java的IO结构中,FilterInputStream 扮演的就是装饰者的角色,简图如下所示


寂然解读设计模式 - 装饰者模式


下面我写一段测试代码,进入源码来梳理下装饰者模式的使用流程

public class Test {
​
 public static void main(String[] args) throws Exception{
​
 DataInputStream dis = new DataInputStream(new FileInputStream("d:\\jiran.txt"));
​
 dis.read();
​
 dis.close();
 }
}

部分源码拷贝,放到代码块中展示出来,如图所示

//可以看到,FileInputStream是InputStream的子类
public
class FileInputStream extends InputStream
{
 /* File Descriptor - handle to the open file */
 private final FileDescriptor fd;

//可以看到,InputStream是抽象类
public abstract class InputStream implements Closeable {
​
//可以看到,FilterInputStream内部聚合了InputStream,即被装饰者
public
class FilterInputStream extends InputStream {
 /**
 * The input stream to be filtered.
 */
 protected volatile InputStream in;

//可以看到,DataInputStream是FilterInputStream的子类
public
class DataInputStream extends FilterInputStream implements DataInput {
源码说明
  • 抽象类 InputStream,类比星巴克案例中的被装饰者 Coffee

  • FileInputStream是InputStream的子类,类比星巴克案例中的各种单品咖啡,是具体的主体

  • FilterInputStream内部聚合了InputStream,扮演装饰者的角色,类比星巴克案例中的Decorator

  • DataInputStream是FilterInputStream的子类,是具体的装饰者,类比星巴克案例中的Milk/Sugar

所以,在JDK的IO体系中,使用到了装饰者模式

下节预告

OK,到这里,装饰者模式的相关内容就结束了,下一节,我们开启组合模式的学习,希望大家能够一起坚持下去,真正有所收获,就像开篇那句话,我走的很慢,但是我从来不后退,哈哈,那我们下期见~

上一篇:寂然解读设计模式 - 代理模式


下一篇:Zookeeper客户端API之读取子节点内容(九)