1. 模式定义⭐️🔴

装饰者模式:动态地将新功能附加到对象上。在对象功能扩展方面,它比继承更有弹性,装饰者模式体现了开闭原则(OCP)❕%%
1032-🏡⭐️◼️涉及到动态概念的设计模式?装饰者模式、策略模式、责任链模式(通过 setter 方法可以动态的切换不同的策略)◼️⭐️-point-202301231032%%

又名包装(Wrapper)模式

2. 模式结构⭐️🔴

  • 抽象构件(Component)角色 :定义一个抽象接口以规范准备接收附加责任的对象
  • 具体构件(Concrete Component)角色 :实现抽象构件,通过装饰角色为其添加一些职责。
  • 抽象装饰(Decorator)角色继承或实现抽象构件,并包含具体构件的实例,可以通过其子类扩展具体构件的功能。
  • 具体装饰(ConcreteDecorator)角色 :实现抽象装饰的相关方法,并给具体构件对象添加附加的责任。
    %%
    1034-🏡⭐️◼️涉及到 2 个结构有 2 种 UML 关系的设计模式:装饰者模式(抽象装饰类【继承 + 聚合】抽象构件类)、组合模式(树枝节点【继承 + 组合】抽象根节点)、解释器模式(非终止表达式【继承 + 聚合】抽象表达式,而且非终止表达式为多个)◼️⭐️-point-202301231034%%

2.1. ⭐️🔴 UML 图示

image.png
%%
1027-🏡⭐️◼️具体装饰类(比如 Milk)是如何计算总价的 ?🔜MSTM📝 抽象装饰类,以组合方式持有被装饰的抽象构件,当具体装饰类包装了具体构件后,就将具体构件传递给抽象装饰类,就可以获取到具体构件的属性 (比如例子中的 price),同时抽象装饰类还可以获取到具体装饰类的属性 (price),就可以完成 cost 业务◼️⭐️-point-202301231027%%

2.2. ⭐️🔴 实现逻辑

继承 + 组合 + 构造导入%%
1030-🏡⭐️◼️装饰者模式的核心逻辑:继承 + 组合 + 构造导入 抽象构件,构造导入:比如具体装饰类 Milk 构造导入具体构建类 LongBlack◼️⭐️-point-202301231030%%

继承的作用是使用抽象构件 Drink 的 cost 以及 setDesc 方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
//抽象装饰类【继承】抽象构件类
public class Decorator extends Drink {
//抽象装饰类【组合】抽象构件类
private Drink obj;
//【构造导入】抽象构件类
public Decorator(Drink obj) { //组合
this.obj = obj;
}

@Override
public float cost() {
// getPrice 自己价格
return getPrice() + obj.cost();
}

@Override
public String getDes() {
// obj.getDes() 输出被装饰者的信息
return des + " " + getPrice() + " && " + obj.getDes();
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class CoffeeBar {  

public static void main(String[] args) {
// TODO Auto-generated method stub
// 装饰者模式下的订单:2份巧克力+一份牛奶的LongBlack

// 1. 点一份 LongBlack
Drink order = new LongBlack();
System.out.println("费用1=" + order.cost());
System.out.println("描述=" + order.getDes());

// 2. order 加入一份牛奶
order = new Milk(order);

System.out.println("order 加入一份牛奶 费用 =" + order.cost());
System.out.println("order 加入一份牛奶 描述 = " + order.getDes());
...
...
}
}

3. 案例分析

  1. 咖啡种类/单品咖啡:Espresso(意大利浓咖啡)、ShortBlack、LongBlack(美式咖啡)、Decaf(无因咖啡)
  2. 调料:Milk、Soy(豆浆)、Chocolate
  3. 要求在扩展新的咖啡种类时,具有良好的扩展性、改动方便、维护方便
  4. 使用 OO 的来计算不同种类咖啡的费用: 客户可以点单品咖啡,也可以单品咖啡 + 调料组合。

3.1. 组合继承方案

image.png

  • 这样设计,会有很多类。当我们增加一个单品咖啡,或者一个新的调料,类的数量就会倍增,出现类爆炸

3.2. 装饰内置方案

image.png

  • 1)方案 2 可以控制类的数量,不至于造成很多的类
  • 2)在增加或者删除调料种类时,代码的维护量很大
  • 3)考虑到用户可以添加多份调料时,可以将 hasMilk 返回一个对应 int
  • 4)考虑使用装饰者模式

3.3. 装饰者模式方案

示例代码:[[Decorator.java]]

装饰者模式下的订单:2 份巧克力 + 一份牛奶的 LongBlack

image.png

说明

  • 1)Milk 包含了 LongBlack
  • 2)一份 Chocolate 包含了 Milk + LongBlack
  • 3)一份 Chocolate 包含了 Chocolate + Milk + LongBlack
  • 4)这样不管是什么形式的单品咖啡 + 调料组合,通过递归方式可以方便的组合和维护

4. ⭐️🔴优缺点

主要优点

  1. 对于扩展一个对象的功能,装饰模式比继承更加灵活性,不会导致类的个数急剧增加
  2. 可以通过一种动态的方式来扩展一个对象的功能,通过配置文件可以在运行时选择不同的具体装饰类,从而实现不同的行为。
  3. 可以对一个对象进行多次装饰,通过使用不同的具体装饰类以及这些装饰类的排列组合,可以创造出很多不同行为的组合,得到功能更为强大的对象。
  4. 具体构件类与具体装饰类可以独立变化,用户可以根据需要增加新的具体构件类和具体装饰类,原有类库代码无须改变,符合 “开闭原则”

主要缺点

  1. 使用装饰模式进行系统设计时将产生很多小对象,每个对象仅负责一部分装饰任务,大量小对象的产生势必会占用更多的系统资源,在一定程序上影响程序的性能
  2. 装饰模式提供了一种比继承更加灵活机动的解决方案,但同时也意味着比继承更加易于出错,排错也很困难,对于多次装饰的对象,调试时寻找错误可能需要逐级排查,较为繁琐。

5. 应用场景⭐️🔴

  • 当不能采用继承的方式对系统进行扩充或者采用继承不利于系统扩展和维护时。

    不能采用继承的情况主要有两类:

    • 第一类是系统中存在大量独立的扩展,为支持每一种组合将产生大量的子类,使得子类数目呈爆炸性增长
    • 第二类是因为类定义不能继承(如 final 类)
  • 在不影响其他对象的情况下,当对象的功能要求可以动态地添加,也可以再动态地撤销时

6. ⭐️🔴框架源码分析

6.1. BufferedWriter

IO 流中的包装类使用到了装饰者模式。BufferedInputStream,BufferedOutputStream,BufferedReader,BufferedWriter

我们以 BufferedWriter 举例来说明,先看看如何使用 BufferedWriter

1
2
3
4
5
6
7
8
9
10
11
12
13
public class Demo {
public static void main(String[] args) throws Exception{
//创建BufferedWriter对象
//创建FileWriter对象
FileWriter fw = new FileWriter("C:\\Users\\Think\\Desktop\\a.txt");
BufferedWriter bw = new BufferedWriter(fw);

//写数据
bw.write("hello Buffered");

bw.close();
}
}

结构
image.png

image.png

小结

BufferedWriter 使用装饰者模式对 Writer 子实现类进行了增强,添加了缓冲区,提高了写数据的效率。

6.2. FilterlnputStream

image.png
由上图可知在 Java 中应用程序通过输入流(InputStream)的 Read 方法从源地址处读取字节,然后通过输出流(OutputStream)的 Write 方法将流写入到目的地址。

流的来源主要有三种:本地的文件(File)、控制台、通过 socket 实现的网络通信

image.png

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 是一个抽象类,即Component
public abstract class InputStream implements Closeable {}
// 是一个装饰类,即Decorator
public class FilterInputStream extends InputStream {
protected volatile InputStream in;
protected FilterInputStream(InputStream in) {
this.in = in;
}
}
// FilterInputStream子类,也继承了被装饰的对象 in
public class DataInputStream extends FilterInputStream implements DataInput {
public DataInputStream(InputStream in) {
super(in);
}

6.2.1. 分析⭐️🔴

  • 1)InputStream 是抽象类,类似我们前面讲的 Drink
  • 2)FileInputStream 是 InputStream 子类,类似我们前面的 DeCaf、LongBlack
  • 3)FilterInputStream 是 InputStream 子类,类似我们前面的 Decorator,修饰者
  • 4)DataInputStream 是 FilterInputStream 子类,类似前面的 Milk,Soy 等,具体的修饰者
  • 5)FilterInputStream 类有 protected volatile InputStream in;,即含被装饰者
  • 6)分析得出在 JDK 的 IO 体系,就是使用装饰者模式

6.3. Mybatis 中的缓存设计

6.3.1. CachingExecutor

image-20210827153314890
CachingExecutor:缓存执行器,装饰器模式,在开启缓存的时候。会在上面三种执行器的外面包上 CachingExecutor;

6.3.2. TransactionalCache

TransactionalCache 是一种缓存装饰器,可以为 Cache 实例增加事务功能。下面分析一下该类的逻辑。

7. 代理和装饰者的区别

静态代理和装饰者模式的区别:

相同点:

  1. 都要实现与目标类相同的业务接口
  2. 在两个类中都要声明目标对象
  3. 都可以在不修改目标类的前提下增强目标方法

不同点:

  1. 目的不同
    装饰者是为了增强目标对象
    静态代理是为了保护和隐藏目标对象
  2. 获取目标对象构建的地方不同
    装饰者是由外界传递进来,可以通过构造方法传递
    静态代理是在代理类内部创建,以此来隐藏目标对象

8. 实战经验

9. 参考与感谢

示例代码:[[Decorator.java]]

设计模式-2、设计模式及设计原则

设计模式 | 装饰者模式及典型应用