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