贫瘠之地

华北无浪漫,死海扬起帆
多少个夜晚,独自望着天

0%

装饰模式 - Decorator

背景

装饰模式(Decorator Pattern)也叫装饰器模式,可以实现在不必改变原类文件和使用继承的情况下,动态地扩展一个对象的功能;它是通过创建一个包装对象(装饰器),也就是装饰来包裹真实的对象(委托对象)

目的 更灵活地对对象方法进行扩展,也可以使多个装饰器共同作用,装饰器之间也可以任意组合

现实世界类比 嵌入式设备,摄像头通过接口连接计算机,安装合适的驱动和软件,整个硬件环境就可以具备摄影机的功能;而在具备摄影功能的基础上再连接新的设备遥控底盘,就变成了可以移动的摄像车

实践

模拟一个场景,需要对对象进行序列化,实现一个序列化工具

问题

假设要求序列化方式有多种实现,这个没问题,继承接口进行多实现

现在要对功能进行拓展,增加对于序列化结果的补充方法(PS.直接改变方法逻辑也是可以的,不过后面代码直接使用的 JSON 实现,不太好设置一个修改方法中间逻辑的例子,所以这样看起来有点像代理模式,后面会有我对于装饰模式和代理模式区别的看法),例如对于结果进行摘要或者输出到文件等操作

在这样的基础上,想要对原有实现方法进行增强、扩展,就需要创建新的实现类,或者再进行一层抽象来达到目的,那如果我需要即进行摘要又进行文件输出呢?

可以看出此时存在的问题:

  • 每次对于方法逻辑的修改可能会变动原有代码
  • 扩展和扩展之间的乘积关系,需要定义更多的抽象
  • 无法动态插拔实现

实现

序列化器接口

只定义一个序列化方法,传入对象输出序列化后的字符串

1
2
3
4
5
public interface Serializer {

String serialize(Object obj);

}

序列化器实现

提供两种不同的实现,一种进行 JSON 序列化,一种调用 toString 方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// JSON 序列化器
public class JsonSerializer implements Serializer {

@Override
public String serialize(Object obj) {
return JSON.toJSONString(obj);
}

}

// toString 序列化器
public class ToStringSerializer implements Serializer {

@Override
public String serialize(final Object obj) {
return obj.toString();
}

}

序列化器装饰

序列化器装饰可以再抽出一个接口继承序列化器接口,这里因为都可以使用同一个 serialize 方法,就不再定义新的接口了

同时对于序列化器装饰基类,可以定义为抽象类,再使用模板方法来由后续实现类来实现

1
2
3
4
5
6
7
8
9
10
11
12
13
public class SerializerDecorator implements Serializer {

private Serializer serializer;

public SerializerDecorator(final Serializer serializer) {
this.serializer = serializer;
}

@Override
public String serialize(final Object obj) {
return serializer.serialize(obj);
}
}

摘要序列化器装饰

重写 serialize 方法,再获取序列化结果后再进行摘要的计算并返回

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class SerializerDigestDecorator extends SerializerDecorator {

private DigestAlgorithm digestAlgorithm;

public SerializerDigestDecorator(final Serializer serializer, DigestAlgorithm digestAlgorithm) {
super(serializer);
this.digestAlgorithm = digestAlgorithm;
}

@Override
public String serialize(final Object obj) {
String s = super.serialize(obj);
// 返回的是序列化后的摘要信息
return DigestUtil.digester(digestAlgorithm).digestHex(s);
}

}

文件输出序列化器装饰

重写 serialize 方法,在获取序列化结果后,输出至文件并返回序列化数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class SerializerFileDecorator extends SerializerDecorator {

private FileWriter fileWriter;

@SneakyThrows
public SerializerFileDecorator(final Serializer serializer, String filePath) {
super(serializer);
this.fileWriter = new FileWriter(new File(filePath));
}

@Override
@SneakyThrows
public String serialize(final Object obj) {
String s = super.serialize(obj);

// 写入文件
fileWriter.write(s);
fileWriter.flush();

return s;
}
}

使用

装饰器和装饰器之间可以任意、不限次数地嵌套,最终的结果会经过每个装饰器和委托对象的处理

暴露相关的方法还可以实现动态地更新装饰器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
public class Run {

public static void main(String[] args) {
// obj
final Student student = new Student("张三", 20);

// JSON 序列化器
final Serializer jsonSerializer = new JsonSerializer();
System.out.println(jsonSerializer.serialize(student));

// 包装 digest 装饰
final Serializer digestDecorator = new SerializerDigestDecorator(jsonSerializer, DigestAlgorithm.SHA1);
System.out.println(digestDecorator.serialize(student));

// 包装 file 装饰
final Serializer fileDecorator = new SerializerFileDecorator(digestDecorator, "./serialize.txt");
System.out.println(fileDecorator.serialize(student));

// to string 序列化器
final Serializer toStringSerializer = new ToStringSerializer();
System.out.println(toStringSerializer.serialize(student));

// 包装 digest 装饰 和 file 装饰
final Serializer toStringFileDecorator =
new SerializerFileDecorator(new SerializerDigestDecorator(toStringSerializer, DigestAlgorithm.MD5),
"./serialize2.txt");
System.out.println(toStringFileDecorator.serialize(student));

}

@Data
@NoArgsConstructor
@AllArgsConstructor
static class Student {

private String name;

private Integer age;

}
}

装饰 & 代理

在这里需要讨论一个问题,装饰模式和代理模式都是对方法进行增强或者扩展,那么它们到底有什么区别?

网络上也有很多讨论,下面是一些观点和我的看法:

  • 组合、聚合是装饰模式,继承是代理模式 看起来有一定道理,感觉装饰模式要想实现嵌套的效果是需要使用组合来实现,但也会存在使用组合来实现的代理模式(静态代理);是不是从一定角度来看的话,组合、聚合、继承这种结构的选择,和实现什么设计模式并无关系?

    The real difference is not ownership (composition versus aggregation), but rather type-information.

    oop - Differences between Proxy and Decorator Pattern - Stack Overflow

  • 装饰模式用于添加功能,代理模式用于增强功能 我觉得不合理,设计模式不应该根据 “规范” 来进行分类,也不会根据模糊的描述来命名

  • 装饰模式在执行期间才会由客户端控制,使装饰器和委托对象建立联系,在此之前装饰器类只知道委托对象的接口;代理模式在编译期间就已经知道代理对象的具体信息(无论这个对象是通过代理类方法创建还是通过注入) 我认为这个是相较而言最有标志性的区别了;装饰的行为发生在代码执行过程中,无论是创建新的装饰器增强委托类,还是取消委托对象的装饰器,都是动态的;而代理类从一开始就明确了自己的委托类

    But a Proxy always knows the (more) specific type of the delegatee. In other words, the Proxy and its delegatee will have the same base type, but the Proxy points to some derived type. A Decorator points to its own base type. Thus, the difference is in compile-time information about the type of the delegatee.

  • 装饰模式的委托对象来自外部,代理模式的委托对象可以自已创建 引申于上一个观点,对于 “具体信息” 的了解就决定了代理对象可以直接创建出委托对象

事实上网络上对于装饰模式、代理模式的界定也一直在讨论中,感觉不一定需要关注到底什么是装饰什么是代理,还是能够使用合理的设计解决

总结

使用装饰模式可以更灵活地对对象方法进行扩展

  • 优点
    • 无需创建新子类即可扩展对象的行为
    • 可以在运行时添加或删除对象的功能
    • 装饰器可以任意组合
    • 可以将实现了许多不同行为的一个大类拆分为多个较小的类来满足单一职责原则
  • 缺点
    • 虽然可以支持运行时添加或删除对象的功能,但是实际实现起来比较困难(装饰栈)
    • 装饰栈一定是有序的
    • 初始化代码变得繁杂

装饰模式的适用环境

  • 在不影响其他对象的情况下,以动态、透明的方式给单个对象添加职责
  • 需要动态地给一个对象增加功能,这些功能也可以动态地被撤销
  • 当不能采用继承的方式对系统进行扩充或者采用继承不利于系统扩展和维护时 不能采用继承的情况主要有两种:第一种是系统中存在大量独立的扩展,为支持每一种组合将产生大量的子类,使得子类数目呈爆炸性增长;第二类是因为类定义不能继承(如 final 类)

参考

装饰设计(装饰者模式 / 装饰器模式) (refactoringguru.cn)

oop - Differences between Proxy and Decorator Pattern - Stack Overflow

设计模式(九)装饰模式(Decorator) · 写最好的设计模式专栏 · 看云 (kancloud.cn)