文章目录
一、装饰者模式(结构型模式)
1.1 场景
有家奶茶店,有波霸奶茶,奶绿、奶昔等种类的奶茶,在点奶茶的时候可以选择加料(珍珠、红豆、仙草等)。
这样的场景下,要求
- 扩展新的奶茶种类时,具有良好的扩展性,改动方便,维护方便
- 使用OO(面向对象)的方法来计算不同种类奶茶的费用,可以单点奶茶,也可以奶茶+配料
1.2 普通解决方案
在不使用设计模式来设计的情况下,我们会使用什么方法呢?
有种解决方式是这样的,首先有一个饮料的抽象类(Drink),如下图
将调料作为饮料的的int型参数,在使用时进行设置数量,计算费用时通过hasXX的方法判断该调料是否为0,0为没有点配料。
这样存在的问题是,在增加和删除调料种类时,代码的维护量很大。每次新增活删除种类都要维护对应调料验证方法和获取设置方法和修改费用计算方法。
1.3 装饰者模式定义
- 装饰者模式:动态的将新功能附加到对象上。在对象功能扩展方面,它比继承更有弹性,装饰者模式也体现了开闭原则。
- 这里提到的动态的将新功能附加到对象和ocp原则(后面会有代码示例)
1.4 装饰者模式(Decorator)原理
- 装饰者模式就像打包一个快递
- 主体: 就是我们的物品(衣服,手机,电脑等)
- 包装: 袋子、箱子、泡沫等
- 组成
- Component: 就是主体 -> 上图的Drink
- ConcreteComponent: 具体的主体 -> 就是具体的饮料(奶茶、奶绿、奶昔)
- Decorator: 装饰者 -> 各种配料(珍珠、红豆、仙草)
- 在如下图的Component和ConcreteComponent之间,如果ConcreteComponent类很多,还可以设计一个缓冲层,将共有的部分提取出来,抽象成一个类(比如奶茶种类太多,就可以抽象一个MilkTea,具体就是草莓奶茶,蓝莓奶茶,这里简单点)。
原来按照正常抽象化的想法来,调料是放在奶茶里的,所以我们抽象化的时候,也可能是把调料放在奶茶里,这样的话我们前面也看到了,会很麻烦(代码维护量大),但是装饰者模式,他反过来看,把配料作为包装,放在外层,包装着主体。
可能有点抽象,看懂了可能也会有疑惑,那这样到好在哪里,多个调料或者奶茶怎么识别?
我们来看一个装饰者模式下的订单: 2份珍珠+1分红豆+1杯奶茶
这里我们就可以看出,为什么装饰者和被装饰者都要实现抽象的Component(Drink)接口了。不管是什么形式的搭配,都可以通过递归的方式计算费用。
二、代码实现
2.1 代码结构
他与上面的类图对应关系大概是
通过上面,我们总结出,装饰者模式有4种类
- 实体抽象类
- 实体实现类
- 装饰者抽象类
- 装饰者实现类
2.2 上代码
1.实体抽象类,这里是Drink
/**
* 饮料的抽象
*/
@Data
public abstract class Drink {
/**
* 奶茶名称
*/
private String name;
private float price = 0.0f;
// get set 省略
// 计算费用的抽象方法 - 子类实现
public abstract float cost(
);
}
2.实体实现类,可以有很多,这里示例一个,奶茶
public class MilkTea extends Drink{
public MilkTea(){
setName("奶茶");
setPrice(10.0f); // 奶茶单价
}
@Override
public float cost() {
return getPrice();
}
}
3.装饰者抽象类,这里是代表配料(重点在这里)
通过这张图
我们知道他大概是个套娃(递归)的形式,所以,他的代码是借助父类的成员变量来存储这所有的商品信息。
/**
* 抽象的装饰类
*/
public class Decorator extends Drink{
private Drink obj;
public Decorator(Drink obj){
this.obj = obj;
}
@Override
public float cost() {
// 由于递归获取,获取本身商品的价格 + 前一个商品(奶茶或者配料)的价格
return getPrice() + obj.cost();
}
@Override
public String getName() {
// 通过借助父类的成员变量存储递归获取的信息
return String.format("%s %s && %s",super.getName(),super.getPrice(),obj.getName());
}
}
4.装饰者实现,这里是具体的配料,只列举一个 - 红豆
/**
* 具体的装饰类 - 红豆
*/
public class RedBean extends Decorator {
public RedBean(Drink obj){
super(obj);
setName("红豆");
setPrice(3.0f); // 调味品价格
}
}
5.准备完成后,就可以模拟外卖点单
// 装饰者模式下,点2份珍珠+1分红豆+1被奶茶
// 1.先点一杯奶茶,先查看信息
Drink milkTea = new MilkTea();
System.out.println("一杯奶茶的价格:" + milkTea.cost());
System.out.println("商品名称:" + milkTea.getName());
// 2.加一份珍珠
Drink order = new Pearl(milkTea);
System.out.println("一杯奶茶+一份珍珠的价格:" + order.cost());
System.out.println("商品名称:" + order.getName());
// 3.加一份珍珠
order = new Pearl(order);
System.out.println("一杯奶茶+两份珍珠的价格:" + order.cost());
System.out.println("商品名称:" + order.getName());
// 4.加一份红豆
order = new RedBean(order);
System.out.println("一杯奶茶+两份珍珠+一份红豆的价格:" + order.cost());
System.out.println("商品名称:" + order.getName());
运行结果
一杯奶茶的价格:10.0
商品名称:奶茶
一杯奶茶+一份珍珠的价格:13.0
商品名称:珍珠 3.0 && 奶茶
一杯奶茶+两份珍珠的价格:16.0
商品名称:珍珠 3.0 && 珍珠 3.0 && 奶茶
一杯奶茶+两份珍珠+一份红豆的价格:19.0
商品名称:红豆 3.0 && 珍珠 3.0 && 珍珠 3.0 && 奶茶
2.3 扩展性
在装饰者模式下,我们需要新增一种配料或者饮料也很简单,模仿配料红豆
比如新增一个配料: 蜂蜜,只需要以下两步(饮料同理)
1.新增实现类
/**
* 具体的装饰类 - 蜂蜜
*/
public class Honey extends Decorator {
public Honey(Drink obj) {
super(obj);
setName("蜂蜜");
setPrice(2.0f);
}
}
2.调用
// 1.先点一杯奶茶,先查看信息
Drink milkTea = new MilkTea();
System.out.println("一杯奶茶的价格:" + milkTea.cost());
System.out.println("商品名称:" + milkTea.getName());
Drink order2 = new Honey(milkTea);
System.out.println("一杯奶茶+一份蜂蜜的价格:" + order2.cost());
System.out.println("商品名称:" + order2.getName());
结果
一杯奶茶的价格:10.0
商品名称:奶茶
一杯奶茶+一份蜂蜜的价格:12.0
商品名称:蜂蜜 2.0 && 奶茶
三、 框架应用
经过了解,我们知道,装饰者模式应用在了Java IO流的设计中,网上找了张图,注释了下,大概是这样的,对应装饰者模式下的各个角色
我们通过读取源码确定了
- InputStream作为抽象主体
- FileInputStream等确实继承了InputStream -> 类似于我们例子中的MilkTea继承Drink -> 作为具体主体
- FilterInputStream继承了InputStream 并聚合了InputStream,类似于Decorator继承Drink并通过聚合引入Drink对象作为成员变量 -> 作为抽象装饰者
- DataInputStream等继承了作为抽象装饰者的FilterInputStream,类似于RedBean继承了Decorator -> 作为具体装饰者