结构型设计模式:关注对象之间的组合方式,以达到构建更大结构的目标,践行“组合优于继承”理念。
结构型设计模式共7种:桥接设计模式,合成设计模式,门面设计模式,享元设计模式,适配器设计模式,装饰器设计模式,代理设计模式。
助记:
- 装饰器、代理
- 桥接、合成、门面
- 适配器、享元
几种易混淆设计模式之间的区分:
装饰器设计模式
VS代理设计模式
:前者是后者的子集
一、桥接设计模式
英文名:Bridge Pattern。
1.1、痛点
业务需求有两个关联的变化维度(大于两个变化维度的处理类似),两个变化维度分别有M和N种可能:
- 以继承方式实现,需要M*N个具体子类
- 践行“组合优于继承”理念,使用组合方式实现,只需要M+N个具体子类
以代码的形式进行描述即:有两个抽象父类A和B,A中具有一个B的引用,A有M个具体子类,B有N个具体子类。
1.2、实现
示例代码如下[1]:
1 | // 抽象父类B:颜色接口 |
1.3、JDK中的例子
- 抽象父类
java.util.logging.Handler
和java.util.logging.Formatter
,在java.util.logging.Handler
中持有一个java.util.logging.Formatter
引用
二、合成设计模式
英文名:Composite Pattern。
很多时候也被称为“组合设计模式”,但易与“Composition over Inheritance / Composite Reuse Principle”的中文名“组合优于继承”产生混淆,故建议将前者中的“Composite”译为“合成”,而将后者中的“Composition / Composite”译为“组合”。
2.1、痛点
有一组部件,部件之间具有整体-部分
的关系,希望以一致的方式处理部分部件
和整体部件
,而不需要进行特殊处理。在该场景中,部分部件和整体部件的共同抽象父类被称为“组件接口”,部分部件被称为“叶子部件”,整体部件被称为“容器部件”。
2.2、实现
示例代码如下[1]:
1 | // 组件接口 |
2.3、JDK中的例子
java.awt.Component
是合成设计模式中的组件接口,java.awt.Container
是相应的容器部件,java.awt.Checkbox
、java.awt.Label
和java.awt.Button
等是相应的叶子部件
三、门面设计模式
英文名:Facade Pattern。
又常被称为“外观设计模式”。
3.1、痛点
提供一个简单易用具有一致性的接口,屏蔽实际实现的复杂性和多样性。在该场景中,有3个角色——Client(业务代码)
,Facade(门面接口)
和具体实现Facade的逻辑
,3个角色的交互是单向的:Client -> Facade -> 具体实现Facade的逻辑
。
3.2、实现
3.2.1、示例代码1[1]
1 | // 子系统:音响 |
3.2.2、示例代码2
1 | package org.slf4j; |
以上是门面日志框架SLF4J的日志门面接口,运行时,框架会绑定(内部通过“适配器设计模式”实现)到根据一定策略找到选中的具体日志框架,比如“Log4j”,“Logback”,“JDK Logging”,“Commons Logging”等。
业务代码使用SLF4J调用日志记录接口时的交互方向是:业务代码 -> org.slf4j.Logger日志门面接口 -> 具体绑定到的日志类
。
使用日志门面接口,得到了一个简单易用具有一致性的日志接口,屏蔽了具体日志实现的多样性。
3.3、JDK中的例子
java.sql.DriverManager
是一个门面接口,屏蔽了操作数据库驱动器实例的复杂性
四、享元设计模式
英文名:Flyweight Pattern。
4.1、痛点
为节约资源,维护一个实例对象池,当欲根据一定条件获取一个实例对象时,先看池中是否有符合条件的实例对象,存在直接返回池中实例对象,不重新生成;不存在则生成一个新实例对象,放入池中并返回。享元设计模式本质就是常说的用于资源优化的池技术。
4.2、实现
示例代码如下[1]:
1 | import java.util.HashMap; |
4.3、JDK中的例子
- 线程池
- 数据库连接池
Byte/Short/Integer/Long/Character/Boolean
类的valueOf()
方法实现中使用了享元设计模式java.lang.String
类的intern()
方法实现中使用了享元设计模式
五、适配器设计模式
英文名:Adapter Pattern。
5.1、痛点
现有一个类A,业务代码只能使用类B,创建一个类C,它继承自类B,内部持有一个类A的实例引用a,其内部方法的实现主要通过调用a的方法实现。
5.2、实现
示例代码如下[1]:
1 | // 统一的Shape接口 |
5.3、JDK中的例子
- 适配器类
java.io.InputStreamReader
:用于将java.io.InputStream
类适配到接口java.io.Reader
上 - 适配器类
java.io.OutputStreamWriter
:用于将java.io.OutputStream
类适配到接口java.io.Writer
上
六、装饰器设计模式
英文名:Decorator Pattern。
6.1、痛点
已有接口A,一个实现接口A的类B,再新建一个实现接口A的类C,作为类B的装饰器类,其内部持有一个类B的实例引用b,其实现方法的形式为简单扩展语句(装饰语句) + 调用b的相应方法
,即实现“在复用类B代码基础上,对类B进行浅度扩展”需求。
6.2、实现
示例代码如下[1]:
1 | // 一个咖啡接口 |
6.3、JDK中的例子
java.io.BufferedReader
对java.io.Reader
进行装饰java.io.BufferedWriter
对java.io.Writer
进行装饰java.io.BufferedInputStream
对java.io.InputStream
进行装饰java.io.BufferedOutputStream
对java.io.OutputStream
进行装饰
七、代理设计模式
英文名:Proxy Pattern。
7.1、痛点
已有接口A,一个实现接口A的类B,再新建一个实现接口A的类C,作为类B的代理类,其内部持有一个类B的实例引用b,其实现方法的形式为复杂扩展语句 + 可能调用也可能不调用b的相应方法
,即实现“在复用类B代码基础上,对类B进行深度扩展”需求。
7.2、实现
有两种实现形式:
- 静态代理
- 动态代理
7.2.1、静态代理
静态代理是指代理关系在编译期确定的代理模式。
示例代码如下[1]:
1 | // 图像接口 |
7.2.2、动态代理
动态代理是指代理类在编译前不存在,代理关系在运行时确定的代理模式。动态代理的实现方案有很多,比如“JDK动态代理”,“CGLIB动态代理”,“Javassist动态代理”和“ASM动态代理”等。
以下是一个使用JDK动态代理实现方案的示例代码:
1 | package me; |
7.3、JDK中的例子
- JDK 1.8中找不到静态代理的例子
- JDK 1.8中能找到很多使用动态代理的例子,比如“
javax.management.JMX
类createProxy
方法中的Object proxy = Proxy.newProxyInstance(interfaceClass.getClassLoader(), interfaces, handler)
语句”
参考文献
[1]https://mp.weixin.qq.com/s/H16g_yZwRMIKagPb8oNIYQ
[2]https://juejin.cn/post/7031543219066404878
[3]https://juejin.cn/post/7029586308854972446