}
}
## 设计模式之结构型模式
结构型模式介绍如何将对象和类组装成较大的结构, 并同时保持结构的灵活和高效
### 适配器模式
使用适配器模式,可以让两个原本不兼容的接口协调工作,结合生活中的例子,适配器模式可以理解为一些接口转换器,例如`typec`转耳机接口,VGA 和 HDMI 的转接器等
适配器模式包含以下角色
- 目标(Target)接口:当前系统业务所期待的接口,它可以是抽象类或接口
- 适配者(Adaptee)类:它是被访问和适配的组件接口,通常是一些第三方接口
- 适配器(Adapter)类:它是一个转换器,通过继承或引用适配者的对象,把适配者接口转换成目标接口,让客户按目标接口的格式访问适配者
例如现在有`TF`卡,但接口程序却针对的是`SD`卡类型,此时就可以使用适配器模式来解决,如下代码
目标(SD卡)接口
```java
/**
* SD 卡驱动程序抽象接口
*
* @author dhj
* @date 2022/9/14
*/
public abstract class SDReaderWriter {
/**
* 读取 SD 卡中的数据
*
* @return 返回 sd 卡中读取的内容
*/
public abstract String reader();
/**
* 向 sd 卡中写入内容
*
* @param content 写入的具体内容
*/
public abstract void writer(String content);
}
适配者(TF卡驱动程序)
import cn.hutool.core.util.RandomUtil;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
/**
* TF 卡驱动程序
*
* @author dhj
* @date 2022/9/14
*/
public class TFReaderWriterService {
private final Map<String, Byte[]> store = new ConcurrentHashMap<>(16);
/**
* 读取 TF 卡中的数据
*
* @return 返回 TF 卡中的字节数据
*/
public List<Byte[]> reader() {
List<Byte[]> bytesList = new ArrayList<>(store.size());
for (Map.Entry<String, Byte[]> entry : store.entrySet()) {
bytesList.add(entry.getValue());
}
return bytesList;
}
/**
* 向 TF 卡中写入数据
*
* @param data 写入的数据
*/
public void writer(Byte[] data) {
store.put(RandomUtil.randomString(16), data);
}
}
适配器
import java.nio.charset.StandardCharsets;
import java.util.*;
/**
* @author dhj
* @date 2022/9/14
*/
public class TFReaderWriterAdapter extends SDReaderWriter {
private static final TFReaderWriterService TF_READER_WRITER_SERVICE = new TFReaderWriterService();
@Override
public String reader() {
List<Byte[]> bytesList = TF_READER_WRITER_SERVICE.reader();
StringBuilder sbf = new StringBuilder();
for (Byte[] bytes : bytesList) {
byte[] tempBytes = new byte[bytes.length];
for (int i = 0; i < bytes.length; i++) {
tempBytes[i] = bytes[i];
}
sbf.append(new String(tempBytes)).append("\n");
}
return sbf.toString();
}
@Override
public void writer(String content) {
byte[] bytes = content.getBytes(StandardCharsets.UTF_8);
Byte[] temp = new Byte[bytes.length];
for (int i = 0; i < bytes.length; i++) {
temp[i] = bytes[i];
}
TF_READER_WRITER_SERVICE.writer(temp);
}
}
客户端使用演示
/**
* @author dhj
* @date 2022/9/14
*/
public class Client {
public static void main(String[] args) {
TFReaderWriterAdapter tfAdapter = new TFReaderWriterAdapter();
tfAdapter.writer("HelloWorld");
String reader = tfAdapter.reader();
System.out.println(reader);
}
}
如上代码中,TF卡的适配器TFReaderWriterAdapter,实现了SDReaderWriter接口,此接口中定义的都是与 SD 卡相关的操作,在 SD卡接口方法的具体实现中,通过引入的TFReaderWriterService实例与 TF 卡的驱动程序交互
最终,客户端使用适配器,调用 SD 卡形式的方法,就能操作 TF 卡了
适配器用于在对接一些第三方库时比较常用,因为第三方库的接口与本地接口风格不一致的可能性较大,如果想在不修改本地接口的情况下继续调用第三方库,就需要为对应的第三方接口编写适配本地接口风格的适配器
桥接模式
当一个类中,存在不同维度的变化时,如果使用继承关系,那么涉及到扩展时,将会产生类爆炸,例如一个抽象类Shape(形状),两个子类分别为Round(圆形) Square(方形),现在对于Shape来说,只有形状这一个维度,使用继承的方式,涉及到扩展时,新增子类即可
可如果现在的需求是不同的形状有红 蓝两种颜色,注意这里多了一个颜色维度,最终的结构图如下
目前只是形状 颜色两个维度,就需要 4 个子类,如果再增加维度,那么子类的数量将会是指数级的,这就是类爆炸
对于这种情况,使用桥接模式再合适不过了
桥接模式将抽象与实现分离,使它们可以独立变化。因为桥接模式主要使用【组合关系代替继承关系】来实现,从而降低了抽象和实现这两个可变维度的耦合
现在将颜色这个维度抽象出来,叫做Color,作为Shape的一个组合属性,那么类结构如下
这样,对某一个维度的扩展,就不会导致另一个维度子类数量的爆炸
桥接模式就是将其他扩展的维度作为另一个抽象类,但一定有一个主要的抽象类来组合分离出去的抽象类,这个主要的抽象类叫做【抽象部分】,例如上述例子中的Shape类,将颜色这个扩展的维度抽象为Color,然后在Shape自身内部组合Color这个属性,这是桥接模式的深刻体现
为了更好的理解桥接模式,可以想象Shape作为两座桥中的A端,Color作为两座桥中的B端,呈现出如下结构
这就好比两座桥,通过组合的方式实现了连接,体现了【桥接】这个概念
在上述例子中,Shape是作为一个高阶控制层存在的,这就好比一个水杯的手柄,手柄就是高阶控制层,也就是【抽象部分】,而杯体作为【实现部分】,通过手柄带动杯体实现喝水的动作,因此桥接模式又称为柄体(Handle and Body)模式
在桥接模式中,有如下几个角色
- 抽象部分,提供高层的控制逻辑,实现则依赖底层的具体实现对象
- 实现部分,代表对某个维度的定义,同时被组合到【抽象部分】中,例如
Shape中的Color维度,就具备红色和蓝色这两个定义 - 具体抽象,对【抽象部分】的实现,主要利用【实现部分】完成控制逻辑,可以简单理解为【抽象部分】的实现类
- 具体实现,包含实现部分中定义的接口的具体逻辑,可以简单理解为【实现部分】的实现类
假设现有一款文本阅读器,需要适配win linux macos的操作系统,并且播放的格式要支持txt epud mobi三种格式,这里面有两个维度,分别为操作系统 文本格式,如果使用继承的方式来定义系统,那么会生成 9 个实现类,任意维度的扩展都会导致整个继承体系中实现类的增加,这就可以使用桥接模式,如下代码
抽象部分(因为用户主要使用不同的操作系统来调用read()来阅读数据,因此Client可以作为一个顶层控制逻辑来定义)
/**
* 文本阅读器客户端抽象类
*
* @author dhj
* @date 2022/9/15
*/
public abstract class Client {
// 组合【文本格式这个维度】这里已经形成了桥接
protected TextDecode textDecode;
public Client(TextDecode textDecode) {
this.textDecode = textDecode;
}
/**
* 阅读书籍
*
* @param book 传入书籍二进制文件
* @return 返回参数书籍的文本表示形式
*/
public abstract String read(byte[] book);
}
具体抽象实现,在这里面,主要使用【抽象部分】中组合的其他维度,如TextDecode,实现【抽象部分】中定义的具体的顶层控制逻辑,例如这里read()方法
/**
* linux 客户端
* @author dhj
* @date 2022/9/15
*/
public class LinuxClient extends Client {
public LinuxClient(TextDecode textDecode) {
super(textDecode);
}
@Override
public String read(byte[] book) {
return textDecode.decode(book);
}
}
/**
* macos 客户端
* @author dhj
* @date 2022/9/15
*/
public class MacOSClient extends Client {
public MacOSClient(TextDecode textDecode) {
super(textDecode);
}
@Override
public String read(byte[] book) {
return textDecode.decode(book);
}
}
/**
* win 客户端
* @author dhj
* @date 2022/9/15
*/
public class WindowsClinet extends Client {
public WindowsClinet(TextDecode textDecode) {
super(textDecode);
}
@Override
public String read(byte[] book) {
return textDecode.decode(book);
}
}
实现部分,在处理【抽象部分】中定义的顶层控制逻辑时,会使用到【实现部分】的具体逻辑
/**
* 文本解析器抽象类
*
* @author dhj
* @date 2022/9/15
*/
public abstract class TextDecode {
/**
* 对传入的数据进行解码
*
* @param content 数据
* @return 返回解码后的文本内容
*/
public abstract String decode(byte[] content);
}
具体实现
/**
* epud 解码器
* @author dhj
* @date 2022/9/15
*/
public class EPUDDecode extends TextDecode {
@Override
public String decode(byte[] content) {
return "epud:" + "(" + new String(content, StandardCharsets.UTF_8) + ")";
}
}
/**
* mobi 解码器
* @author dhj
* @date 2022/9/15
*/
public class MOBIDecode extends TextDecode {
@Override
public String decode(byte[] content) {
return "mobi(" + new String(content, StandardCharsets.UTF_8) + ")";
}
}
/**
* txt 解码器
* @author dhj
* @date 2022/9/15
*/
public class TXTDecode extends TextDecode {
@Override
public String decode(byte[] content) {
return "txt(" + new String(content, StandardCharsets.UTF_8) + ")";
}
}
客户端调用
/**
* @author dhj
* @date 2022/9/15
*/
public class MainDemo {
public static void main(String[] args) {
Client winClient = new WindowsClinet(new EPUDDecode());
String read = winClient.read("红楼梦".getBytes(StandardCharsets.UTF_8));
System.out.println(read);
}
}
如果后续要扩展阅读器,例如支持
Android系统,那么也只需要再实现一个AndroidClient的子类即可,无需再针对每个格式再去实现诸如AndroidTXTDecoderAndroidEPUDDecoder等类型的解码器,因为解码器是通过【组合】的形式,桥接到Clinet中的桥接模式实际上遵循的是软件设计原则中的【合成复用原则】
组合模式
组合模式(Composite Pattern),又叫部分整体模式,它创建了对象组的树形结构,将对象组合成树状结构以表示【整体-部分】的层次关系;在组合模式构成的对象树中,顶层的节点叫做root节点,root节点下的节点叫做树枝节点(存在子节点)和树叶节点(没有子节点)
尽管由组合模式构建出的对象树具备较深的层次,但客户端总是可以使用统一的接口来操作对象树,这是因为无论是顶层的 root 节点还是树枝节点或树叶节点,都实现了统一的【抽象构件】接口,此接口定义了组成对象树中的每个元素统一的行为方式
在组合模式中,有如下角色
- 抽象构件(Component):它的主要作用是为树叶构件和树枝构件声明公共接口,并实现它们的默认行为。抽象构件还声明访问和管理子类的接口(但抽象构件本身不允许操作)
- 树枝构件(Composite):是组合中的分支节点对象,它有子节点,用于继承和实现抽象构件,但可能不具有树叶构件的某些行为。它的主要作用是存储和管理子部件,通常包含 Add()、Remove()、GetChild() 等方法
- 树叶构件(Leaf):是组合中的叶节点对象,它没有子节点,用于继承或实现抽象构件,定义了组合内元素的行为
组合模式的最大优点在于无需了解构成树状结构的对象的具体类,也无需了解对象是简单还是复杂的;只需调用通用接口以相同的方式对其进行处理即可。 当调用该方法后,对象会将请求沿着树结构 逐层传递 下去
下面以大学 -> 系部 -> 专业为例,使用组合模式定义和实现这三者之间的关系以及操作方法
定义抽象构件
/**
* 抽象构件
*
* @author dhj
* @date 2022/9/16
*/
public abstract class Component {
/**
* 构件名称
*/
protected String name;
/**
* 构件描述
*/
protected String desc;
protected String getName() {
return name;
}
protected String getDesc() {
return desc;
}
public Component(String name, String desc) {
this.name = name;
this.desc = desc;
}
/**
* 添加构件
*
* @param component 添加的构件
*/
protected abstract void add(Component component);
/**
* 删除构件
*
* @param component 删除的构件
*/
protected abstract void remove(Component component);
/**
* 显示当前节点下的所有构件信息
*/
protected abstract void show();
}
定义树枝构件
/**
* root构件(某一大学),也就是 root 节点
*
* @author dhj
* @date 2022/9/16
*/
public class University extends Component {
/**
* 树枝构件(某一大学下某一系部)
*/
private final List<Component> departments;
public University(String name, String desc) {
super(name, desc);
this.departments = new ArrayList<>(10);
}
@Override
protected void add(Component component) {
departments.add(component);
}
@Override
protected void remove(Component component) {
departments.remove(component);
}
@Override
protected void show() {
System.out.println("名称【" + getName() + "】,描述【" + getDesc() + "】");
// 传递
for (Component department : departments) {
department.show();
}
}
}
/**
* 树枝构件(某一大学下某一系部)
*
* @author dhj
* @date 2022/9/16
*/
@ToString
public class Department extends Component {
/**
* 树叶构件(某一系部下某一所属专业)
*/
private final List<Component> disciplines;
public Department(String name, String desc) {
super(name, desc);
this.disciplines = new ArrayList<>(10);
}
@Override
protected void add(Component component) {
this.disciplines.add(component);
}
@Override
protected void remove(Component component) {
this.disciplines.remove(component);
}
@Override
protected void show() {
System.out.println("名称【" + getName() + "】,描述【" + getDesc() + "】");
// 传递
for (Component discipline : this.disciplines) {
discipline.show();
}
}
}
定义树叶构件
/**
* 树叶构件(某一大学某一系部下某一专业)
*
* @author dhj
* @date 2022/9/16
*/
public class Discipline extends Component {
public Discipline(String name, String desc) {
super(name, desc);
}
@Override
protected void add(Component component) {
throw new RuntimeException("树叶节点无法操作");
}
@Override
protected void remove(Component component) {
throw new RuntimeException("树叶节点无法操作");
}
@Override
protected void show() {
// 叶子节点无法传递, 直接处理具体业务
System.out.println("名称【" + getName() + "】,描述【" + getDesc() + "】");
}
}
测试类
/**
* @author dhj
* @date 2022/9/16
*/
public class MainDemo {
public static void main(String[] args) {
University university = new University("麻省理工", "世界一流大学");
Department department = new Department("信息工程系", "世界一流系部");
university.add(department);
department.add(new Discipline("软件工程","世界一流专业"));
university.show();
}
}
装饰模式
装饰模式的装饰二字体现在对具体组件外层的装饰,这就好比一个人(抽象组件)张三(具体组件),他本身就具备走路和跑步的功能,此时在张三的脚上再穿了一双鞋子,这个行为可以称之为【装饰】,而这个鞋子称之为【装饰者】,张三称之为【被装饰者】,通过【装饰】,张三跑得更快了,所以装饰模式适合于功能增强的场景
装饰者也不仅仅只限于装饰具体组件,也可以装饰其他装饰者,层层嵌套,装饰者的方法有一部分是自己的,这属于它的功能,然后调用被装饰者的方法实现,从而也保留了被装饰者的功能;具体组件应当是装饰层次的最低层,因为只有具体组件的方法实现不需要依赖于其它对象
装饰者的作用就在于对被装饰者原本的功能进行扩展或增强,例如在被装饰者的功能执行前后做一些事情,装饰者的结构如下图
简单来说,装饰者就是在被装饰者外面套了一层,就像穿衣服或者对一个东西进行装扮
标准的装饰者模式具备以下几种角色
- 抽象组件,申明公共接口
- 具体组件,提供组件中申明的公共接口的默认实现
- 基础装饰类,对具体组件进行装饰(关联一个【抽象组件】类型的常用变量),主要定义关联的具体组件的封装接口
- 具体装饰类,定义了可动态添加到部件的额外行为。 具体装饰类会重写装饰基类的方法, 并在调用父类方法之前或之后进行额外的行为
在 JDK 的 IO 流中,就存在对装饰模式的应用,其中InputStream作为抽象组件,定义了基本的公共接口;FileInputStream作为具体组件同时也作为基础装饰类,实现了InputStream中的公共接口;BufferedInputStream中关联了InputStream类型的常用变量,并通过构造方法的形式进行初始化,额外的扩展的 IO 操作时的缓存功能,然而底层的 IO 操作依然由其关联属性FileInputStream实例来完成,BufferedInputStream则作为一个装饰者装饰FileInputStream对其功能进行增强
下面以一个数据读取器为例,演示装饰者模式的相关代码,如下
抽象组件
/**
* 抽象组件
*
* @author dhj
* @date 2022/9/18
*/
public abstract class DataReaderWriter {
/**
* 读取数据,返回数据的读取结果
*
* @return 返回读取的结果集
*/
public abstract List<String> reader();
/**
* 写入数据
*
* @param data 被写入的数据
*/
public abstract boolean writer(String data);
}
具体组件
/**
* 具体组件,实现抽象组件中的公共接口
*
* @author dhj
* @date 2022/9/18
*/
public class BaseDataReaderWriter extends DataReaderWriter {
// 保存写入的数据
protected final List<String> store = new ArrayList<>(10);
@Override
public List<String> reader() {
return this.store;
}
@Override
public boolean writer(String data) {
return store.add(data);
}
}j
基础装饰
/**
* 基础装饰,实现抽象组件中定义的接口,但在接收到对应请求时,委派给具体组件
*
* @author dhj
* @date 2022/9/18
*/
public class DataReaderWriterDecorator extends DataReaderWriter {
protected DataReaderWriter wrapper;
public DataReaderWriterDecorator(DataReaderWriter wrapper) {
this.wrapper = wrapper;
}
@Override
public List<String> reader() {
return wrapper.reader();
}
@Override
public boolean writer(String data) {
return wrapper.writer(data);
}
}
装饰者
/**
* @author dhj
* @date 2022/9/18
*/
public class EncryDataReaderWriter extends DataReaderWriterDecorator {
private static final String key = "dwhdgy672dbshtht";
public EncryDataReaderWriter(DataReaderWriter wrapper) {
super(wrapper);
}
@Override
public List<String> reader() {
List<String> reader = super.reader();
// 获得结果后解密数据
reader.replaceAll(enstr -> SecureUtil.aes(key.getBytes(StandardCharsets.UTF_8)).decryptStr(enstr));
return reader;
}
@Override
public boolean writer(String data) {
// 在保存之前加密数据
String encryptStr = SecureUtil.aes(key.getBytes(StandardCharsets.UTF_8)).encryptBase64(data);
return super.writer(encryptStr);
}
}
测试类
/**
* @author dhj
* @date 2022/9/18
*/
public class MainDemo {
public static void main(String[] args) {
DataReaderWriter dataReaderWriter = new EncryDataReaderWriter(new BaseDataReaderWriter());
dataReaderWriter.writer("HelloWorld");
List<String> reader = dataReaderWriter.reader();
reader.forEach(System.out::println);
}
}
以上是一个普通的数据读取器,通过装饰者模式扩展加密解密功能的例子,可以看到,使用装饰者模式,既能保证抽象组件中的接口不变,又能得到功能上的扩展
后续在想要扩展功能时,也无需修改现有组件,继续创建装饰类按需关联现有的装饰者或具体组件即可,满足开闭原则
外观模式
现在有一个第三方库或者子系统,以为其中的接口或逻辑极为复杂,使得客户端调用的难度增加,影响了客户端的系统复杂性,此时就可以为这个第三方库或子系统设计一个【外观】,外观中暴露简单的客户端所需的接口,提供给客户端调用,在简单接口中则实现客户端直接调用的复杂逻辑
外观模式有如下几个角色
- 外观,提供了一种访问特定子系统功能的便捷方式
- 子系统,由若干对象或复杂逻辑构成,如果要用这些对象完成有意义的工作, 你必须深入了解子系统的实现细节,比如按照正确顺序初始化对象和为其提供正确格式的数据,子系统类不会意识到外观的存在
- 客户端,使用外观代替对子系统对象的直接调用
下面以看电影举例子,电影院在播放电影时,可能会进行一系列操作,比如搭建影台、放置胶片、调整放映机、最后放映,这一系列操作客户端不应该关系,可以创建一个【外观】,客户端通过【外观】实现直接观看电影,如下代码
子系统
/**
* 子系统
*
* @author dhj
* @date 2022/9/18
*/
public class MovieShow {
/**
* 搭建影台
*/
public MovieShow buildStage() {
System.out.println("build stage");
return this;
}
/**
* 放置胶片
*/
public MovieShow setFilm() {
System.out.println("set film");
return this;
}
/**
* 调整放映机
*/
public MovieShow adjustTheProjector() {
System.out.println("adjust the projector");
return this;
}
/**
* 开始放映
*/
public void startShow() {
System.out.println("start show");
}
}
外观
/**
* 电影放映系统的【外观】类
*
* @author dhj
* @date 2022/9/18
*/
public class MovieShowFacade {
private final MovieShow movieShow;
public MovieShowFacade(MovieShow movieShow) {
this.movieShow = movieShow;
}
/**
* 一键观看电影
*/
public void movieShow() {
movieShow.buildStage().setFilm().adjustTheProjector().startShow();
}
}
客户端
/**
* 客户端,一键观看电影
*
* @author dhj
* @date 2022/9/18
*/
public class Client {
public static void main(String[] args) {
MovieShowFacade movieShowFacade = new MovieShowFacade(new MovieShow());
movieShowFacade.movieShow();
}
}
通过外观模式,将放映电影的一系列繁琐的步骤都封装到对应的【外观】中,客户端只需要通过【外观】中的movieShow()即可完成子系统的一系列调用
如果一个外观类过于臃肿,可以按照一定规则,创建其他外观类,例如按照不同的行为区分创建外观类
外观模式符合迪米特法则,以为客户端和子系统并没有直接交互的必要,通过第三方来间接调用子系统,达到解耦的效果
享元模式
享元模式主要在大量创建不可变或可重用对象时使用,是一种运用共享的方式有效地支持大量细粒度对象创建的模式
享元模式和原型模式类似,都是在节省创建对象的成本,但原型模式通过clone的形式每次创建的都是相同内容的对象,享元模式则需要区分【内在状态】和【外在状态】,内在状态是能够在【多个对象中共享】的状态,也称之为【享元】,外在状态则通过享元对象中定义的【享元方法】传递
在jdk中,存在着许多对享元模式的应用,如Integer.valueOf(),默认会缓存-128 ~ 127的Integer实例对象,用于快速返回此范围内的Integer实例
参照Integer,模拟一个对象缓存,如下代码
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
/**
* @author dhj
* @date 2022/9/22
*/
public class StudentCache {
private static final Map<String, Student> stuCache = new ConcurrentHashMap<>(16);
public Student fetchStu(Integer id, String name) {
String key = id + name;
Student student = stuCache.get(key);
if (student == null) {
student = new Student(id, name);
stuCache.put(key, student);
return student;
}
return student;
}
static class Student {
private final Integer stuId;
private final String name;
public Student(Integer stuId, String name) {
this.stuId = stuId;
this.name = name;
}
@Override
public String toString() {
return "Student{" + "stuId=" + stuId + ", name='" + name + '\'' + '}';
}
}
}
测试类
/**
* @author dhj
* @date 2022/9/22
*/
public class MainDemo {
public static void main(String[] args) {
StudentCache studentCache = new StudentCache();
StudentCache.Student zhangsan = studentCache.fetchStu(1, "zhangsan");
System.out.println(zhangsan);
StudentCache.Student zhangsan1 = studentCache.fetchStu(1, "zhangsan");
System.out.println(zhangsan == zhangsan1); // true
}
}
代理模式
代理模式是一种结构型设计模式,能够实现在原始业务逻辑执行的前后,通过代理的方式执行额外的逻辑而无需修改原始代码,如下图
代码模式在Spring中有大量应用,Spring中各种功能增强的实现,例如aop,事务,前置处理器,后置处理器等都利用了代理模式实现
下面使用【照片查看器】演示代理模式的应用,照片查看器会在目标图片加载完成之前,使用默认图片渲染,代码如下
抽象图片类
/**
* @author dhj
* @date 2022/9/22
*/
public abstract class Image {
/**
* 返回图片是否加载完成
*
* @return true 完成,false 未完成
*/
public abstract boolean isLoad();
/**
* 显示图片
*/
public abstract void show();
}
目标图片
/**
* @author dhj
* @date 2022/9/22
*/
public class TargetImage extends Image {
private final String url;
private final long startTime;
public TargetImage(String url) {
this.url = url;
this.startTime = System.currentTimeMillis();
}
@Override
public boolean isLoad() {
long endTime = System.currentTimeMillis();
return endTime - startTime >= 3000;
}
@Override
public void show() {
System.out.println(url);
}
}
目标图片的【代理对象】
/**
* @author dhj
* @date 2022/9/22
*/
public class ProxyImage extends Image {
// 被代理的目标对象
private final TargetImage targetImage;
private static final String defaultUrl = "https://image/default.jpg";
public ProxyImage(TargetImage targetImage) {
this.targetImage = targetImage;
}
@Override
public boolean isLoad() {
return true;
}
@Override
public void show() {
// 在目标图片加载完成之前,显示默认图片
while (!targetImage.isLoad()) {
System.out.println(defaultUrl);
SleepHelper.sleep(100);
}
System.out.println("------load finish--------");
targetImage.show();
}
}
测试类
/**
* @author dhj
* @date 2022/9/22
*/
public class MainDemo {
public static void main(String[] args) {
ProxyImage proxyImage = new ProxyImage(new TargetImage("https://images/target.jepg"));
proxyImage.show();
}
}
设计模式之行为型模式
行为模式负责对象间的高效沟通和职责委派
责任链模式
责任链模式负责将目标调用所对应的一系列步骤或环节组合起来,每一个环节中,通过成员变量的形式保存对下一个环节的引用,同时由当前环节决定是否调用下一环节,请求会在所有环节构成的【执行链】上移动,直至完成整个流程或者在某一个环节中断,如下图
这类似于单向链表,通过这种结构能够完成多步骤处理的逻辑且更为灵活,责任链中的每一个环节都可以方便的替换和删除,便于扩展,下面以一个登录认证授权为例,简单使用责任链模式
import lombok.Data;
/**
* 客户端类,携带 token 和 permission 发起请求
* @author dhj
* @date 2022/9/23
*/
@Data
public class ClientRequest {
private String token;
private String permission;
public ClientRequest(String token, String permission) {
this.token = token;
this.permission = permission;
}
}
抽象处理器(定义每一个处理环节的公共接口)
/**
* @author dhj
* @date 2022/9/23
*/
public abstract class Handler {
protected Handler nextHandler;
public Handler(Handler nextHandler) {
this.nextHandler = nextHandler;
}
public abstract boolean handler(ClientRequest req);
}
具体处理器(实现抽象处理器中定义的接口,可以有多个,组成【责任链】)
/**
* 简单的登录认证处理器
* @author dhj
* @date 2022/9/23
*/
public class AuthHandler extends Handler {
public AuthHandler(Handler nextHandler) {
super(nextHandler);
}
@Override
public boolean handler(ClientRequest req) {
if (req.getToken() != null) {
System.out.println("auth check pass!");
return nextHandler.handler(req);
}
System.out.println("auth check fail");
return false;
}
}
/**
* 简单的权限认证处理器
* @author dhj
* @date 2022/9/23
*/
public class PermissionHandler extends Handler {
public PermissionHandler(Handler nextHandler) {
super(nextHandler);
}
@Override
public boolean handler(ClientRequest req) {
if ("user.list".equals(req.getPermission())) {
System.out.println("permission chech pass");
return true;
}
System.out.println("permission check fail");
return false;
}
}
客户端
/**
* @author dhj
* @date 2022/9/23
*/
public class MainDemo {
public static void main(String[] args) {
// 组装责任链
AuthHandler authHandler = new AuthHandler(new PermissionHandler(null));
ClientRequest clientRequest = new ClientRequest("4278dhxfgytef34", "user.list");
// 处理客户端请求
boolean handler = authHandler.handler(clientRequest);
if (handler) {
System.out.println("request success");
} else {
System.out.println("request fail");
}
}
}
命令模式
在命令模式中,将不同的处理逻辑对应的请求封装成命令,再通过调用者发送命令来触发对应的处理逻辑,将请求与具体的处理逻辑分离开
这样,每个具体逻辑都由指定的命令来触发,而这些命令又由调用者统一管理,这代表其他模块也能通过调用者来调用指定的处理逻辑,而不是说将这些具体逻辑的调用耦合在某个单一的模块中
这就好比遥控器与空调,用户作为请求者,只需要按下遥控器上的按钮,就可以控制空调的开关,这里的按钮就相当于【命令】,用户按下按钮,相当于发送一个【命令】,至于按下按钮后,遥控器作为一个【调用者】通过此命令调用了哪个【具体逻辑】,用户并不需要关心;最后空调会接收到命令,它作为一个接收者,负责真正的执行具体逻辑
命令模式有如下几个角色
- 抽象命令(Command)定义了命令的接口和执行命令的抽象函数
execute() - 具体命令(ConcreteCommand)实现了【抽象命令】中的所有方法,并管理一个【接收者(Receiver)】对象,通过调用接收者(Receiver)的功能来完成命令的具体操作
- 接收者(Receiver)接收者负责接收并执行命令的具体操作,是业务逻辑的处理类
- 调用者(Invoker)即发送请求的类,它管理着命令对象,并通过调用命令对象来执行请求,请注意,它不能直接访问接收者
下面通过一个简单的股票交易,演示命令模式的使用
定义抽象命令
/**
* 股票交易的抽象命令
*
* @author dhj
* @date 2022/9/24
*/
public abstract class Command {
public abstract void execute();
}
定义接收者,用于处理具体业务逻辑
/**
* 接收者,负责处理具体的业务逻辑
*
* @author dhj
* @date 2022/9/24
*/
public class Receive {
/**
* 买入股票
*
* @param orderId 股票代码
* @param amount 买入数量
*/
public void buy(String orderId, long amount) {
System.out.printf("买入【%s】,数量【%d】\n", orderId, amount);
}
/**
* 卖出股票
*
* @param orderId 股票代码
* @param amount 卖出数量
*/
public void sell(String orderId, long amount) {
System.out.printf("卖出【%s】,数量【%d】\n", orderId, amount);
}
}
定义具体命令,某一个命令可以调用接收者中的一个或多个处理逻辑
/**
* 股票买入命令
*
* @author dhj
* @date 2022/9/24
*/
public class BuyCommand extends Command {
private final Receive receive;
private final String orderId;
private final long amount;
public BuyCommand(Receive receive, String orderId, long amount) {
this.receive = receive;
this.orderId = orderId;
this.amount = amount;
}
@Override
public void execute() {
receive.buy(this.orderId, this.amount);
}
}
/**
* 股票卖出命令
*
* @author dhj
* @date 2022/9/24
*/
public class SellCommand extends Command {
private final Receive receive;
private final String orderId;
private final long amount;
public SellCommand(Receive receive, String orderId, long amount) {
this.receive = receive;
this.orderId = orderId;
this.amount = amount;
}
@Override
public void execute() {
receive.sell(this.orderId, this.amount);
}
}
定义调用者,用于统一管理以及执行命令
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.function.Consumer;
/**
* @author dhj
* @date 2022/9/24
*/
public class Invoker {
private final List<Command> commandList = Collections.synchronizedList(new ArrayList<>(10));
/**
* 创建一个交易请求
*
* @param command 交易请求【分为买入、卖出】
*/
public void createTransaction(Command command) {
commandList.add(command);
}
/**
* 执行所有的交易请求
*/
public void hanlders() {
commandList.forEach(Command::execute);
}
}
客户端通过调用者发送命令
/**
* @author dhj
* @date 2022/9/24
*/
public class MainDemo {
public static void main(String[] args) {
Receive receive = new Receive();
BuyCommand buyCommand = new BuyCommand(receive, "1013131", 10);
SellCommand sellCommand = new SellCommand(receive, "29h2dtr33", 23);
Invoker invoker = new Invoker();
invoker.createTransaction(buyCommand);
invoker.createTransaction(sellCommand);
invoker.hanlders();
}
}
在客户端中,只需要选择自己所需要的命令,交给调用者,即可完成命令对应处理逻辑的调用
迭代器模式
迭代器主要用于屏蔽不同结构的集合中获取元素的底层逻辑,向客户端暴露统一的集合迭代接口,因为集合的类型有很多种,如【数组】【图】【链表】【队列】【栈】等,不同的集合之间遍历的形式会存在一定差异,而通过定义抽象的迭代器接口,就能够消除这种差异
代码示例如下
定义抽象迭代器
/**
* @author dhj
* @date 2022/9/25
*/
public interface Iterator<T> {
/**
* 是否还有下一个元素
*
* @return true 存在下一个元素,false 没有下一个元素
*/
boolean hasNext();
/**
* 获取下一个元素
*
* @return 返回元素
*/
T next();
}
自定义动态数组
/**
* @author dhj
* @date 2022/9/25
*/
public class MyArrayList<T> {
private Object[] elements;
private int size = 0;
private static final double LOAD_FACTOR = 0.75; // 负载因子,超过该值,执行扩容逻辑
public MyArrayList() {
this.elements = new Object[10];
}
public MyArrayList(int capacity) {
this.elements = new Object[capacity];
}
public int add(T data) {
if ((double) (this.size + 1) / (double) this.elements.length >= LOAD_FACTOR) {
Object[] expanElements = new Object[this.elements.length * 2];
System.arraycopy(this.elements, 0, expanElements, 0, this.size);
this.elements = expanElements;
}
this.elements[size++] = data;
return this.size;
}
public T get(int index) {
if (index > this.size - 1) {
throw new IndexOutOfBoundsException();
}
return (T) this.elements[index];
}
// 返回具体迭代器实例,用于客户端统一调用迭代器接口遍历集合
public Iterator<T> iterator(){
return new Itr();
}
// 使用内部类的形式,实现迭代器接口,定义当前集合自己的具体迭代逻辑
private class Itr implements Iterator<T> {
private int pointer = 0;
@Override
public boolean hasNext() {
return pointer <= size - 1 && elements[pointer] != null;
}
@Override
public T next() {
return (T) elements[pointer++];
}
}
}
测试代码
/**
* @author dhj
* @date 2022/9/25
*/
public class MainDemo {
public static void main(String[] args) {
MyArrayList<String> list = new MyArrayList<>(20);
for (int i = 0; i < 30; i++) {
list.add(String.valueOf(i));
}
Iterator<String> iterator = list.iterator();
while (iterator.hasNext()) {
String next = iterator.next();
System.out.println(next);
}
}
}
中介者模式
中介者模式用于将对象间复杂的关系分离,将这些关系都统一交给【中介者】处理,相当于将网状结构分离为星型结构,如下
中介者模式有如下四种角色
- 抽象中介者:抽象中介者定义了统一的接口,以及一个或者多个事件方法,用于各同事类之间的通信
- 具体中介者:实现了抽象中介者所声明的事件方法,协调各同事类之间的行为,持有所有同事类对象的引用
- 抽象同事:定义了抽象同事类,持有抽象中介者对象的引用
- 具体同事:继承抽象同事类,实现自己的业务,通过中介者跟其他同事类进行通信
以一个在线点餐系统为例,顾客通过统一的在线点餐平台【中介者】点餐,再由中介者通知对应的商家出餐,据此演示中介者模式的相关代码
抽象业务类,定义基本业务接口
/**
* @author dhj
* @date 2022/9/25
*/
public abstract class Shop {
/**
* 商家接单
*
* @param foodName 菜品名称
*/
abstract void orderReceiving(String foodName);
/**
* 获取商家名称
*
* @return 商家名称
*/
abstract String getName();
}
定义具体业务类
/**
* @author dhj
* @date 2022/9/25
*/
public class ShopA extends Shop {
private final String name;
public ShopA(String name) {
this.name = name;
}
@Override
void orderReceiving(String foodName) {
System.out.printf("【%s】接单,【%s】准备出餐\n", this.name, foodName);
}
@Override
String getName() {
return this.name;
}
}
/**
* @author dhj
* @date 2022/9/25
*/
public class ShopB extends Shop {
private final String name;
public ShopB(String name) {
this.name = name;
}
@Override
void orderReceiving(String foodName) {
System.out.printf("【%s】接单,【%s】准备出餐\n", this.name, foodName);
}
@Override
String getName() {
return this.name;
}
}
抽象中介者,定义协调、调用所有业务类的基本接口
/**
* 在线点餐抽象中介者
*
* @author dhj
* @date 2022/9/25
*/
public abstract class FoodOnline {
/**
* 商家入驻在线点餐平台
*
* @param shop 入驻的商家
*/
abstract void settled(Shop shop);
/**
* 用户点餐
*
* @param shopName 商家名称
* @param foodName 菜品名称
*/
abstract void placeOrder(String shopName, String foodName);
}
具体中介者
import java.util.HashMap;
import java.util.Map;
/**
* 在线点餐系统具体中介者
*
* @author dhj
* @date 2022/9/25
*/
public class MeiTuanFoodOnline extends FoodOnline {
private final Map<String, Shop> shopMap = new HashMap<>(16);
@Override
void settled(Shop shop) {
if (shopMap.containsKey(shop.getName())) {
System.out.println("不允许重复入驻");
return;
}
shopMap.put(shop.getName(), shop);
}
@Override
void placeOrder(String shopName, String foodName) {
if (!shopMap.containsKey(shopName)) {
System.out.println("商家未入驻");
return;
}
shopMap.get(shopName).orderReceiving(foodName);
}
}
客户端通过在线点餐系统【中介者】点餐
/**
* @author dhj
* @date 2022/9/25
*/
public class MainDemo {
public static void main(String[] args) {
Shop shopA = new ShopA("老麻抄手");
Shop shopB = new ShopB("沙县小吃");
FoodOnline foodOnline = new MeiTuanFoodOnline();
foodOnline.settled(shopA);
foodOnline.settled(shopB);
foodOnline.placeOrder("隆江猪脚饭", "双拼猪肘饭");
foodOnline.placeOrder("老麻抄手", "海味抄手");
foodOnline.placeOrder("沙县小吃", "烤鸭饭");
}
}
输出
商家未入驻 【老麻抄手】接单,【海味抄手】准备出餐 【沙县小吃】接单,【烤鸭饭】准备出餐
通过在线点餐这个中介者,将所有的商家都统一管理起来,客户端只需要调用【中介者】中统一的接口便可完成对应的业务逻辑
备忘录模式
【备忘录】模式是在不破坏封装性的前提下,捕获一个对象的内部状态,并在该对象之外保存这个状态。这样以后就可以将该对象恢复到原先的状态,例如现在大多数文件编辑器都具备的撤销功能,利用的就是备忘录模式
备忘录模式有如下几个角色
*备忘录(Memento)*负责存储【发起者】对象的内部状态,并可以防止除【发起者】自身以外的其他对象访问备忘录
*发起者(Originator)*负责创建一个备忘录用以记录当前时刻它自身的内部状态,并可以使用备忘录恢复内部状态
*备忘录管理者(Caretaker)*负责保存所有备忘录
下面通过备忘录模式,模拟一个简单的git版本管理工具,代码如下
发起者
import lombok.Builder;
import lombok.Data;
import lombok.experimental.Accessors;
import java.util.Date;
/**
* 代码提交者【发起者】
*
* @author dhj
* @date 2022/9/25
*/
@Data
@Builder
@Accessors(chain = true)
public class CodeOriginator {
/**
* 版本号
*/
private String version;
/**
* 提交的代码内容
*/
private String codeContent;
/**
* 版本提交时间
*/
private Date commitDate;
/**
* 创建并提交当前代码版本【备忘录】
*
* @param caretaker 【备忘录】管理者
*/
public void createAndCommit(CodeCaretaker caretaker) {
caretaker.commit(new CodeMemo(this.version, this.codeContent, this.commitDate));
}
/**
* 回退到指定版本
*
* @param version 指定版本
* @param caretaker 【备忘录管理者】
* @return 返回是否回退成功
*/
public boolean goBackVersion(String version, CodeCaretaker caretaker) {
CodeMemo memoByVersion = caretaker.getMemoByVersion(version);
if (memoByVersion == null) {
System.out.println("回退失败,此版本不存在");
return false;
}
this.codeContent = memoByVersion.getCodeContent();
this.version = memoByVersion.getVersion();
this.commitDate = memoByVersion.getCommitDate();
System.out.printf("回退成功,当前版本【%s】\n", this.version);
return true;
}
}
备忘录
import lombok.Getter;
import lombok.Setter;
import java.util.Date;
/**
* 代码版本备忘录(用于记录某次代码提交版本的全部信息)
*
* @author dhj
* @date 2022/9/25
*/
@Getter
@Setter
public class CodeMemo {
/**
* 版本号
*/
private String version;
/**
* 提交的代码内容
*/
private String codeContent;
/**
* 版本提交时间
*/
private Date commitDate;
public CodeMemo(String version, String codeContent, Date commitDate) {
this.version = version;
this.codeContent = codeContent;
this.commitDate = commitDate;
}
}
管理者
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
/**
* 代码版本管理者,用于统一管理代码版本
*
* @author dhj
* @date 2022/9/25
*/
public class CodeCaretaker {
private static final Map<String, CodeMemo> codeMemoMap = new ConcurrentHashMap<>(16);
/**
* 提交指定的代码版本
*
* @param codeMemo 代码版本提交的备忘录实例
*/
public void commit(CodeMemo codeMemo) {
if (codeMemoMap.containsKey(codeMemo.getVersion())) {
System.out.println("版本冲突");
return;
}
codeMemoMap.put(codeMemo.getVersion(), codeMemo);
}
/**
* 获取指定版本的代码
*
* @param version 版本号
* @return 返回指定版本的代码备忘录实例
*/
public CodeMemo getMemoByVersion(String version) {
if (!codeMemoMap.containsKey(version)) {
return null;
}
return codeMemoMap.get(version);
}
}
客户端提交&回退代码到指定版本
import java.util.Date;
/**
* @author dhj
* @date 2022/9/25
*/
public class MainDemo {
public static void main(String[] args) {
// 先创建管理者,用于统一管理代码【备忘录】
CodeCaretaker codeCaretaker = new CodeCaretaker();
// 创建一个代码版本【发起者】
CodeOriginator codeOriginator =
CodeOriginator.builder()
.version("1.0")
.codeContent("<h1>Hello World</h1>")
.commitDate(new Date()).build();
// 提交本次代码版本【发起者】保存自身内部状态
codeOriginator.createAndCommit(codeCaretaker);
// 改动代码
codeOriginator.setCodeContent("<h1>Hello World</h1><br/><span>hello world</span>")
.setVersion("1.1")
.setCommitDate(new Date());
// 再次提交版本
codeOriginator.createAndCommit(codeCaretaker);
System.out.println(codeOriginator); // 当前最新提交为 1.1 版本
// 版本回退【发起者】恢复内部状态
if (codeOriginator.goBackVersion("1.0", codeCaretaker)) {
System.out.println(codeOriginator); // 回退到 1.0 版本
}
}
}
观察者模式
观察者模式,又称发布订阅模式,在对象之间定义了一对多的依赖,这样一来,当一个对象改变状态,依赖它的对象会收到通知并执行指定的操作
例如张三订阅了一个音乐频道,那么在音乐频道更新时,张三会收到短信或者邮箱的通知,张三可以选择收听更新的内容或者什么也不做,而没有订阅这个音乐频道的人则不会被通知到,音乐频道和订阅者之间存在一对多的关系
观察者模式有如下角色
抽象发布者(Subject):也就是一个抽象主题,它把所有对观察者对象的引用保存在一个集合中,每个主题都可以有任意数量的观察者。抽象主题提供一个接口,可以增加和删除观察者角色。一般用一个抽象类和接口来实现
具体发布者(ConcreteSubject):也就是【抽象发布者】的实现,当主题的内部状态改变时,向所有登记过的【观察者】发出通知
抽象观察者(Observer):为所有的【具体观察者】定义一个接口,此接口用于申明【观察者】在得到【发布者】的主题通知时所作的行为
具体观察者(ConcreteObserver):实现【抽象观察者】中定义的接口,执行接收到主题通知时的具体逻辑
代码演示如下
抽象观察者
/**
* 抽象观察者
*
* @author dhj
* @date 2022/9/26
*/
public abstract class Observer {
/**
* 观察者接收订阅的通知
*/
public abstract void receive(String notice);
}
抽象发布者
/**
* 抽象发布者
*
* @author dhj
* @date 2022/9/26
*/
public abstract class Publisher {
/**
* 发布主题内容,通知主题对应的观察者【订阅者】
*
* @param notice 发布的通知内容
*/
public abstract void publish(String notice);
/**
* 添加观察者订阅主题
*
* @param observer 观察者实例
*/
public abstract void addObserver(Observer observer);
/**
* 取消观察者的主题订阅
*
* @param observer 观察者对象
*/
public abstract void cancelObserver(Observer observer);
}
具体发布者
import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;
/**
* @author dhj
* @date 2022/9/26
*/
public class ConcretePublisher extends Publisher {
List<Observer> observerList = new CopyOnWriteArrayList<>();
@Override
public void publish(String notice) {
for (Observer observer : observerList) {
observer.receive(notice);
}
}
@Override
public void addObserver(Observer observer) {
observerList.add(observer);
}
@Override
public void cancelObserver(Observer observer) {
observerList.remove(observer);
}
}
具体观察者
/**
* @author dhj
* @date 2022/9/26
*/
public class ConcreteObserver extends Observer {
private final String name;
public ConcreteObserver(String name) {
this.name = name;
}
@Override
public void receive(String notice) {
System.out.printf("【%s】接收到订阅的通知:【%s】\n", this.name, notice);
}
}
状态模式
状态模式与 状态机 的概念紧密相连,可以参考 电子签中台认证环节的状态机实践 - 掘金 (juejin.cn)
而状态模式的主要思想在于事物任意时刻仅可处于几种有限的状态中,在任何一个特定状态中,事物的行为都不相同,且可瞬间从一个状态切换到另一个状态;不过,根据当前状态,程序可能会切换到指定一种状态,也可能会保持当前状态不变,这些数量有限且预先定义的状态切换规则被称为【转移】
以一个简单的糖果售卖机为例,有如下【状态转移表】
| 行为\状态 | 未投币 | 已投币 | 已出货 | 售空 |
|---|---|---|---|---|
| 投币 | yes | no | no | no |
| 退币 | no | yes | no | no |
| 按下按钮 | no | yes | no | no |
| 取货 | no | no | yes | no |
上述表格就规定了一个糖果售卖业务模型在不同状态下的各种行为约定,如果单纯使用if-else来处理,仅仅是这几种简单行为的状态转移,也会造成大量的分支代码,不利于维护和扩展,使用状态模式就可轻易解决
状态模式能够在发生状态切换时,改变事物的行为,以适应当前的状态或者直接进入下一状态,例如张三购买了某个商品,如果他不支付,那么当前商品会一直或在指定时间内处于【待支付状态】,一旦张三支付了该商品,那么状态就会变更为【已支付状态】,同时进入【待发货状态】,此时这个商品对象关联的行为应该变为【收货】而不是【支付】,这种行为的变化正是由【状态变更】所导致的
状态模式具备以下几个角色
- 上下文:上下文中保存了对具体状态的引用,并会将与当前状态相关的工作委派给它
- 抽象状态:定义任意状态下的公共接口,业务逻辑的通用接口通常在此声明,也就是对应【状态转移表】中的【行为】
- 具体状态:具体实现【状态】中的接口,同时【具体状态】中也可以反向引用上下文对象,用于转移到某一状态时,由当前状态自动将上下文转移到下一个状态
下面使用状态模式的相关代码演示上面的【糖果售卖机】案例
定义抽象状态,覆盖糖果机在任何状态下的可执行操作
/**
* 定义糖果售卖机的抽象状态
* 并在其中定义任意状态下的公共行为
*
* @author dhj
* @date 2022/9/28
*/
public abstract class State {
/**
* 投币
*/
public abstract void coin();
/**
* 退币
*/
public abstract void refund();
/**
* 按下按钮
*/
public abstract void down();
/**
* 取货
*/
public abstract void pickUp();
}
定义具体状态
/**
* 已投币状态
*
* @author dhj
* @date 2022/9/28
*/
public class CoinedState extends State {
private final CandyContext context;
public CoinedState(CandyContext context) {
this.context = context;
}
@Override
public void coin() {
System.out.println("已投币,不要重复投币!");
}
@Override
public void refund() {
this.context.setState(new NotCoinedState(this.context));
System.out.println("已退币,取消出货");
}
@Override
public void down() {
this.context.setState(new ShippedState(this.context));
System.out.println("正在出货,请稍等");
}
@Override
public void pickUp() {
System.out.println("请按下按钮出货");
}
}
/**
* 糖果机未投币状态【有限状态中的默认状态,也叫起始状态】
*
* @author dhj
* @date 2022/9/28
*/
public class NotCoinedState extends State {
private final CandyContext context;
public NotCoinedState(CandyContext context) {
this.context = context;
}
@Override
public void coin() {
this.context.setState(new CoinedState(this.context));
System.out.println("投币成功,按下按钮出货");
}
@Override
public void refund() {
System.out.println("未投币,无法退币!");
}
@Override
public void down() {
System.out.println("请先投币");
}
@Override
public void pickUp() {
System.out.println("未投币,无法取货!");
}
}
/**
* 糖果机已出货状态
*
* @author dhj
* @date 2022/9/28
*/
public class ShippedState extends State {
private final CandyContext context;
public ShippedState(CandyContext context) {
this.context = context;
}
@Override
public void coin() {
System.out.println("请先取货后,再次投币");
}
@Override
public void refund() {
System.out.println("已出货,无法退币");
}
@Override
public void down() {
System.out.println("已出货,请勿重复出货");
}
@Override
public void pickUp() {
context.setStock(context.getStock() - 1); // 扣减库存
context.setState(new NotCoinedState(this.context)); // 当前为【终止状态】,结束后切换上下文为初始状态
System.out.println("取货成功,可再次投币");
}
}
/**
* 糖果机售空状态【有限状态中的终止状态】
*
* @author dhj
* @date 2022/9/28
*/
public class ShortSaleState extends State {
@Override
public void coin() {
System.out.println("已售空,请等待补货");
}
@Override
public void refund() {
System.out.println("未投币,无法退币!");
}
@Override
public void down() {
System.out.println("请先投币");
}
@Override
public void pickUp() {
System.out.println("未投币,无法取货");
}
}
定义上下文对象,其引用了状态的实例,并借用此实例完成对应状态的操作
/**
* 糖果机可以包含所有状态,这里作为上下文对象
*
* @author dhj
* @date 2022/9/28
*/
public class CandyContext {
private State state;
private Integer stock;
public CandyContext(Integer stock) {
this.stock = stock;
}
public void setState(State state) {
this.state = state;
}
public void setStock(Integer stock) {
this.stock = stock;
}
public Integer getStock() {
return stock;
}
/**
* 给糖果机投币
*/
public void sell() {
System.out.println("-----投币操作------");
if (this.stock > 0) {
this.state.coin();
} else {
System.out.println("无货,请联系商家补货");
this.state = new ShortSaleState();
}
}
/**
* 糖果机退币
*/
public void refund() {
System.out.println("-----退币操作------");
this.state.refund();
}
/**
* 按下取货按钮
*/
public void down() {
System.out.println("-------出货操作-------");
this.state.down();
}
/**
* 从糖果机取货
*/
public void pickUp() {
System.out.println("------取货操作-------");
this.state.pickUp();
}
}
测试类,模拟糖果机操作
/**
* @author dhj
* @date 2022/9/28
*/
public class MainDemo {
public static void main(String[] args) {
// 初始化指定库存:10 & 初始状态:【未支付】
CandyContext context = new CandyContext(10);
context.setState(new NotCoinedState(context));
// 直接取货【失败】
context.pickUp();
// 直接退币【失败】
context.refund();
// 投币后再退币【成功】
context.sell();
context.refund();
// 投币,按下按钮,取货【成功】
context.sell();
context.down();
context.pickUp();
// 投币,退币,取货【失败】
context.sell();
context.refund();
context.pickUp();
}
}
策略模式
如果现在对同一业务的不同情况有不同的处理方式,通常情况下,可能会直接通过if条件判断来根据对应情况编写代码逻辑,如下
if(xxxx){ // 情况1
// 业务代码1
}else if(xxxx){ // 情况2
// 业务代码2
}else if(xxxx){ // 情况3
// 业务代码3
}else{ // 其他情况
// 业务代码
}
并且可能分支里面涉及到的代码逻辑又会存在其他分支情况,这使得不同情况的代码之间高度耦合,后续如果业务规则发生变化,就需要直接更改已经写好的代码逻辑,违反了开闭原则,且因为修改了旧代码,还会导致回归测试
针对这种情况,使用【策略模式】是比较合适的,策略模式中定义了多种不同的算法以适应被不同的情况所调用,同时通过一个上下文对象维护这些【策略】,而后在涉及到具体逻辑的地方,直接使用上下文对象获取对应的策略来处理,后续业务发生变化,也只需要更改对应的策略或者扩展新的策略而无需修改业务层的代码结构,如下
// 设置策略
context.setStrategy(strategyA);
// 使用指定的策略处理相应的业务逻辑
context.hanlder();
// ----- 业务情况发生变化时 ------
// 重新设置策略
context.setStrategy(strategyB);
// 处理变化后的业务逻辑
context.hanlder();
策略模式有以下角色
- 抽象策略(Strategy):策略类,通常是一个接口或者抽象类,声明了不同策略通用的接口
- 具体策略(ConcreteStrategy)角色:实现了策略类中的策略方法,封装相关的算法和行为
- 上下文(Context)角色:持有一个策略类的引用,最终给客户端调用,且根据情况切换策略的引用对象
不过,通常情况下,策略模式都会和简单工厂模式相结合,目的就是为了更方便的获取【具体策略】,同时将策略的【创建、设置】与具体的使用分离
下面以某一商品根据不同的优惠规则计算价格的业务,来演示策略模式
优惠参数配置类,用于配置不同优惠模式下计算价格所需的参数【业务相关】
import lombok.Getter;
import lombok.experimental.Accessors;
import java.math.BigDecimal;
/**
* 折扣配置类,用于配置指定类型的折扣相关的数值
* 例如【满减】类型的折扣,需要配置【满多少】、【减多少】两项配置
*
* @author dhj
* @date 2022/9/28
*/
@Getter
@Accessors(chain = true)
public class DiscountConfig {
//======================满减配置=============================
/**
* 总价满多少【满减配置1】
*/
private BigDecimal thresholdAmount;
/**
* 总价减去多少【满减配置2】
*/
public BigDecimal reductionAmount;
//======================折扣配置==============================
/**
* 折扣比【折扣配置1】
*/
public BigDecimal discount;
/**
* 最大折扣金额【折扣配置2】
*/
public BigDecimal maxDiscountAmount;
//========================无门槛配置===========================
/**
* 总价直接抵扣多少【无门槛配置】
*/
public BigDecimal deductionAmount;
/**
* 满减配置
*
* @param thresholdAmount 总价满多少
* @param reductionAmount 总价减多少
* @return 返回当前实例
*/
public DiscountConfig fullReductionConfig(String thresholdAmount, String reductionAmount) {
this.thresholdAmount = new BigDecimal(thresholdAmount);
this.reductionAmount = new BigDecimal(reductionAmount);
return this;
}
/**
* 折扣配置
*
* @param discount 折扣比【discount %】
* @param maxDiscountAmount 最大折扣金额
* @return 返回当前实例
*/
public DiscountConfig discountConfig(String discount, String maxDiscountAmount) {
this.discount = new BigDecimal(discount);
this.maxDiscountAmount = new BigDecimal(maxDiscountAmount);
return this;
}
/**
* 无门槛配置
*
* @param deductionAmount 直接抵扣的金额
* @return 返回当前实例
*/
public DiscountConfig noThresholdConfig(String deductionAmount) {
this.deductionAmount = new BigDecimal(deductionAmount);
return this;
}
}
抽象策略
import java.math.BigDecimal;
/**
* 价格计算【抽象策略】
*
* @author dhj
* @date 2022/9/28
*/
public abstract class CalculationStrategy {
/**
* 计算价格
*
* @param totalAmount 总价
* @param config 优惠参数
* @return 返回最终价格
*/
public abstract BigDecimal compute(BigDecimal totalAmount, DiscountConfig config);
}
具体策略【折扣价计算策略】
import java.math.BigDecimal;
import java.math.RoundingMode;
/**
* 折扣价格计算策略
*
* @author dhj
* @date 2022/9/28
*/
public class DiscountStrategy extends CalculationStrategy {
@Override
public BigDecimal compute(BigDecimal totalAmount, DiscountConfig config) {
BigDecimal discountAmount = totalAmount
.multiply(config.getDiscount().divide(new BigDecimal("100"), 2, RoundingMode.HALF_DOWN));
// 如果折扣超出限制,以最大折扣金额计算
if (totalAmount.subtract(discountAmount).compareTo(config.getMaxDiscountAmount()) > 0) {
return totalAmount.subtract(config.getMaxDiscountAmount());
}
// 返回折扣价
return discountAmount;
}
}
具体策略【满减计算策略】
import java.math.BigDecimal;
/**
* 满减价格计算策略
*
* @author dhj
* @date 2022/9/28
*/
public class FullReductionStrategy extends CalculationStrategy {
@Override
public BigDecimal compute(BigDecimal totalAmount, DiscountConfig config) {
// 总价小于满减金额,不计算优惠
if (totalAmount.compareTo(config.getThresholdAmount()) < 0) {
return totalAmount;
}
// 减去优惠的金额
return totalAmount.subtract(config.getReductionAmount());
}
}
具体策略【无门槛计算策略】
import java.math.BigDecimal;
/**
* 无门槛折扣价格计算策略
*
* @author dhj
* @date 2022/9/28
*/
public class NoThresholdStrategy extends CalculationStrategy {
@Override
public BigDecimal compute(BigDecimal totalAmount, DiscountConfig config) {
return totalAmount.subtract(config.getDeductionAmount());
}
}
优惠类型
/**
* 折扣类型
*
* @author dhj
* @date 2022/9/28
*/
public enum DiscountType {
/**
* 满减
*/
FULL_FEDUCTION,
/**
* 打折
*/
DISCOUNT,
/**
* 无门槛
*/
NO_THRESHOLD
}
策略工厂,方便获取指定的价格计算策略【可以理解为简化操作后的上下文对象】
import java.util.HashMap;
import java.util.Map;
/**
* 价格计算策略工厂
*
* @author dhj
* @date 2022/9/28
*/
public class PriceComputeStrategyFactory {
private final static Map<DiscountType, CalculationStrategy> strategyMap;
static {
strategyMap = new HashMap<>(3);
strategyMap.put(DiscountType.DISCOUNT, new DiscountStrategy());
strategyMap.put(DiscountType.FULL_FEDUCTION, new FullReductionStrategy());
strategyMap.put(DiscountType.NO_THRESHOLD, new NoThresholdStrategy());
}
/**
* 获取价格计算策略
*
* @param type 策略枚举类型
* @return 返回价格计算策略的实例
*/
public static CalculationStrategy getComputeStrategy(DiscountType type) {
return strategyMap.get(type);
}
}
测试类
import java.math.BigDecimal;
/**
* @author dhj
* @date 2022/9/28
*/
public class MainDemo {
public static void main(String[] args) {
// 总价
BigDecimal totalAmount = new BigDecimal("500.00");
// 多种优惠方案参数配置
DiscountConfig config = new DiscountConfig()
.discountConfig("8", "100") // 折扣参数
.noThresholdConfig("15") // 无门槛参数
.fullReductionConfig("100", "10"); // 满减参数
// 通过简单工厂模式获取对应的价格计算策略,再通过策略计算最终价格
String priceStr = PriceComputeStrategyFactory
.getComputeStrategy(DiscountType.DISCOUNT).compute(totalAmount, config).toPlainString();
System.out.println(priceStr);
}
}
策略模式和状态模式比较相像,它们都在上下文中维护了针对不同情况而创建的实例且都能够进行切换,但不同的是,状态模式的各个状态实例与上下文之间,以及各个状态实例之间都存在耦合关系,其中一个状态的变动会触发切换到另一个状态
策略模式则不一样,各个策略之间相互独立,只是视情况切换策略,策略中也无需保存对上下文的引用来判断当前上下文的情况
模板方法模式
假设有一电商平台,同时支持【微信支付】【支付宝支付】等多个第三方支付,那么在支付发起的过程中,可能还会进行一些其他步骤,例如检查订单是否超时,订单是否已支付等检查步骤,还有锁库存,构建支付参数,记录结果等一系列步骤,最后会根据支付方式发起支付请求,代码模拟如下
// 锁定库存
storeService.lock();
// 构建支付参数
buildParams();
// 一系列检查
check();
// 发起支付请求
if(WeXinPay){
// 微信支付请求
weixinPay();
}else if(AliPay){
// 支付宝支付请求
aliPay();
}
// 保存支付结果
save();
而在上面这一系列流程中,唯有发起支付请求时,具体的逻辑会有所不同,因为存在多种支付方式或者支付通道,这就可以使用模板方法模式来优化
在模板方法模式中,将一系列算法或步骤实现提供好,并通过一个统一的模板方法进行统一调用,而这一系列的算法或步骤可以是已经实现好的,也可以是抽象的,通常会将通用的步骤默认实现好,将需要自定义的步骤申明为抽象的,以便留给子类实现,如下形式
// 模板方法,统一执行定义好的步骤
void templateMethod(){
step1();
step2();
step3();
}
void step1(){
// 默认实现
}
void step2(){
// 默认实现
}
// 需要自定义的步骤,留给子类实现
abstract void step3();
模板方法模式具备如下角色
- 抽象类(AbstractClass) 会声明作为算法步骤的方法, 以及依次调用它们的实际模板方法。 算法步骤可以被声明为
抽象类型, 也可以提供一些默认实现 - 具体类(ConcreteClass) 可以重写所有步骤, 但不能重写模板方法自身
模板方法跟建造者模式很相似,也是定义了一系列步骤,但建造者模式中定义的全部是抽象步骤,具体步骤的实现和调用顺序交给子类和Director来实现;模板方法直接在抽象父类中就定义好了步骤的具体实现和调用顺序且子类无法更改调用顺序,这也是模板方法的体现:通过一个给定的模板,在此基础上进行修改但不能完全脱离模板
下面通过模拟不同支付方式的支付流程,来演示模板方法的基本使用
抽象类
/**
* 抽象的支付类
*
* @author dhj
* @date 2022/9/29
*/
public abstract class AbstractPayService {
/**
* 支付的模板方法,统一调用支付过程中的一系列步骤
*/
public final void pay() {
lockStore();
String param = buildParam();
if (check(param)) {
sendPay(param);
} else {
System.out.println("支付参数异常,请检查订单信息");
}
}
/**
* 构建支付参数
*
* @return 返回构建好的支付参数
*/
public String buildParam() {
System.out.println("构建支付参数");
return "params";
}
/**
* 检查支付参数
*
* @param params 支付参数
* @return true 检查通过
*/
public boolean check(String params) {
System.out.println("检查支付参数");
return !params.isEmpty();
}
/**
* 锁定库存
*/
public void lockStore() {
System.out.println("库存锁定");
}
/**
* 发送支付请求
*
* @param param 支付参数
*/
public abstract void sendPay(String param);
}
注意模板方法
pay()被final修饰,子类无法更改,其余不想要子类更改的步骤也可如此
实现类
/**
* 支付宝支付实现类
*
* @author dhj
* @date 2022/9/29
*/
public class AliPayService extends AbstractPayService {
@Override
public void sendPay(String param) {
System.out.println("支付宝支付请求,参数:" + param);
}
}
/**
* 微信支付实现类
*
* @author dhj
* @date 2022/9/29
*/
public class WeiXinPayService extends AbstractPayService {
@Override
public void sendPay(String param) {
System.out.println("微信支付请求,参数:" + param);
}
}
测试
/**
* @author dhj
* @date 2022/9/29
*/
public class MainDemo {
public static void main(String[] args) {
AbstractPayService payService = new WeiXinPayService();
System.out.println("---------------微信支付--------------------");
payService.pay();
payService = new AliPayService();
System.out.println("---------------支付宝支付-------------------");
payService.pay();
}
}
打印结果(符合预期)
---------------微信支付-------------------- 库存锁定 构建支付参数 检查支付参数 微信支付请求,参数:params ---------------支付宝支付------------------- 库存锁定 构建支付参数 检查支付参数 支付宝支付请求,参数:params
模板方法模式虽然能够对不同逻辑的调用实现解耦,但如果情况变化过多,会增加大量的子类,导致系统臃肿,适用于某些步骤虽然有变化,但变化的情况却比较固定的时候使用
访问者模式
现在具有某种类型的数据结构,我们需要访问此结构中的元素或内容,最能想到的简单办法,就是为结构中的元素定义某种方法,在此方法中编写访问当前元素内容的逻辑,然后获取所有的元素,依次调用该方法
但是这种方法破坏了此元素类的单一职责(定义的访问方法与元素类自身的职责没有太大关系),同时也不符合开闭原则(修改了元素类原本的结构)
可现在就是需要访问每个元素,并且元素的实际类型可能还不相同;或许可以换一种思路,将元素的【访问逻辑】分离出来,然后在元素中提供一个可接收【访问逻辑】传入的方法,然后将自身的一些信息或者直接是整个自身的引用传给【访问逻辑】,这能最大限度的减少【访问逻辑】与元素自身职责的耦合,同时能够控制向【访问逻辑】暴露的信息,这个【访问逻辑】指的就是【访问者】
【访问者】模式是一种 能将算法与其所作用的对象隔离开来 的行为型设计模式,访问者模式有如下角色
- 抽象访问者,定义对访问的目标元素的通用访问行为,其参数可以设置为要访问的元素
- 具体访问者,实现【抽象访问者】中定义的具体逻辑
- 抽象节点,定义了接收【访问者】访问的方法
- 具体节点,具体实现【抽象节点】中定义的访问方法,这说明凡实现了【抽象节点】的【具体节点】都能被访问者访问
- 结构对象,包含了多个节点,同时提供给访问者访问多个节点的方法
下面以访问不同文件夹中的文件,来简单演示访问者模式的基本代码结构
抽象节点,也就是文件
/**
* 抽象节点【文件】
*
* @author dhj
* @date 2022/10/1
*/
public abstract class SysFile {
protected String name;
public SysFile(String name) {
this.name = name;
}
public abstract void accept(Folder folder);
}
抽象访问者,通过文件夹来访问文件,文件夹就相当于文件的【访问者】
/**
* 文件夹【抽象访问者】
*
* @author dhj
* @date 2022/10/1
*/
public abstract class Folder {
public abstract void visit(SysFile file);
}
具体节点
/**
* 具体节点,指的是不同的文件【学习文件】
*
* @author dhj
* @date 2022/10/1
*/
public class LearnFile extends SysFile {
public LearnFile(String name) {
super(name);
}
@Override
public void accept(Folder folder) {
folder.visit(this);
}
}
/**
* 具体节点,指的是不同的文件【工作文件】
*
* @author dhj
* @date 2022/10/1
*/
public class WorkFile extends SysFile {
public WorkFile(String name) {
super(name);
}
@Override
public void accept(Folder folder) {
folder.visit(this);
}
}
具体访问者,针对不同的节点类型,定义不同的访问者
/**
* 针对【学习文件】的访问者
*
* @author dhj
* @date 2022/10/1
*/
public class LearnFolder extends Folder {
@Override
public void visit(SysFile file) {
if (file instanceof LearnFile) {
System.out.println("学习文件:" + file.name);
}
}
}
/**
* 针对【工作文件】的访问者
*
* @author dhj
* @date 2022/10/1
*/
public class WorkFolder extends Folder {
@Override
public void visit(SysFile file) {
if (file instanceof WorkFile) {
System.out.println("工作文件:" + file.name);
}
}
}
结构对象
/**
* 文件管理器【结构对象】
*
* @author dhj
* @date 2022/10/1
*/
public class FileManage {
private final List<SysFile> fileList = new ArrayList<>(10);
// 添加元素
public FileManage add(SysFile file) {
fileList.add(file);
return this;
}
// 提供给访问者访问元素的方法
public void openFolder(Folder folder) {
fileList.forEach(folder::visit);
}
}