java中装饰者模式详解原理_寂然解读设计模式 - 装饰者模式

I walk very slowly, but I never walk backwards

复制代码

设计模式 - 装饰者模式

寂然

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

案例需求 - 星巴克咖啡

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

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

配料:Milk、sugar

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

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

解决方案一:一般实现

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

1a114cd4107d99bebcf59d210b11fd14.png

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

解决方案二:配料改进

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

10f38f676493c0a5648cc33629b39320.png

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

方案分析

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

基本介绍

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

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

原理类图

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

8219ed6810e3fc184ec0df56dc5e014a.png

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

装饰者模式角色

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

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

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

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

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

解决方案三:装饰者模式

类图展示

18ff7e4b768ae7e5e1fadca355d41612.png

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

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

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

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

装饰者模式下订单思路

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

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

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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值