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

简介: 装饰者模式:在不改变原有对象的基础之上,动态的将功能附加到对象上,提供了比继承更有弹性的替代方案(扩展原有对象功能) ,装饰者模式也体现了开闭原则(ocp)这里提到的动态的将新功能附加到对象和 ocp 原则,在后面的应用实例上会以代码的形式体现
I walk very slowly, but I never walk backwards 

设计模式 - 装饰者模式


寂然

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

案例需求 - 星巴克咖啡

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

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

  • 配料:Milk、sugar

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

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

解决方案一:一般实现

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


1606704625987.png


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

解决方案二:配料改进

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


1606708772146.png


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

方案分析

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

基本介绍

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

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

原理类图

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


1607411924062.png


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

装饰者模式角色

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

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

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

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

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

解决方案三:装饰者模式

类图展示

1607413475053.png


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

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

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

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

装饰者模式下订单思路

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


1607414309774.png


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 扮演的就是装饰者的角色,简图如下所示


1607420024749.png


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

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

相关文章
|
6月前
|
设计模式 Java
Java设计模式【十】:装饰者模式
Java设计模式【十】:装饰者模式
48 0
|
9天前
|
设计模式 Java Kotlin
Kotlin教程笔记(56) - 改良设计模式 - 装饰者模式
Kotlin教程笔记(56) - 改良设计模式 - 装饰者模式
19 0
|
16天前
|
设计模式 Java Kotlin
Kotlin 学习笔记- 改良设计模式 - 装饰者模式
Kotlin 学习笔记- 改良设计模式 - 装饰者模式
24 0
|
19天前
|
设计模式 Java Kotlin
Kotlin教程笔记(56) - 改良设计模式 - 装饰者模式
Kotlin教程笔记(56) - 改良设计模式 - 装饰者模式
21 0
|
5月前
|
设计模式 Java API
程序技术好文:设计模式:装饰者模式
程序技术好文:设计模式:装饰者模式
25 0
|
6月前
|
设计模式 Java
【设计模式系列笔记】装饰者模式
装饰者模式是一种结构型设计模式,它允许你通过将对象放入包含行为的特殊封装类中来为原始对象添加新的行为。这种模式可以动态地将责任附加到对象上,而不影响其它对象。
70 11
|
6月前
|
设计模式 缓存 安全
设计模式-代理模式(静态代理、动态代理、cglib代理)、代理模式和装饰者模式的区别
设计模式-代理模式(静态代理、动态代理、cglib代理)、代理模式和装饰者模式的区别
|
6月前
|
设计模式
【设计模式】装饰者模式
【设计模式】装饰者模式
|
设计模式
23种设计模式_MODE08装饰者模式_手写代码实现
23种设计模式_MODE08装饰者模式_手写代码实现
|
6月前
|
设计模式 Go 开发工具
Golang设计模式——22装饰者模式
Golang设计模式——22装饰者模式
52 0