分享 Java 开发中常用到的设计模式(一)
前言
不知道大家在开发的时候,有没有想过(遇到)这些问题:
- 大家都是按需要开发,都是一个职级的同事,为什么有些人的思路就很清晰,代码也很整洁、易懂;而自己开发,往往不知道怎么下手设计,写完了也是bug一堆,codereview的时候更是频频被怼...
- 感觉每天都是CURD,写重复的代码,做类似的需求,怎么才能提高自己的水平?
- 每每看到大佬的代码,或者优秀框架的源码,总是似懂非懂,怀疑自己是不是缺少了哪些知识?
如果你有这些问题,或者思考过这些问题,那么你起码意识到了自己的不足,这其实是没有熟练掌握软件开发中的重要技能-设计模式而导致的。
先大致来看一下关于设计模式的思维导图,包括六大软件开发原则、设计模式分类以及23种设计模式的名称:
作为软件开发人员,看着这些设计模式的名称,你是否多少会有一些印象呢?
今天就和大家一起分享一下几个在 Java 开发中常见的设计模式,包括具体的应用场景、代码 demo 以及 UML 图解等。
一、观察者模式
1.1基本概念
观察者模式,即Observer模式,顾名思义,指的是:当被观察对象发生变化时,会告知观察者。适用于根据对象状态进行相应处理的场景。
1.2Demo案例
下面用一个demo来直观地展示观察者模式的实际应用场景:观察者将会观察一个能生成分数的对象,并且观察者还可以通过观察,得到不同形式的分数结果(数字形式、图示形式)。
类和接口一览,如表1.1所示:
名称 | 说明 |
---|---|
Observer | 观察者的接口,感知来自被观察对象的状态变化 |
AbstractScoreGenerator | 被观察者的抽象类 |
ScoreGenerator | 被观察者的具体功能实现 |
DigitalObserver | 具体的观察者,感知被观察者的变化,用数值的形式实现 |
GraphObserver | 具体的观察者,感知被观察者的变化,用图示的形式实现 |
Main | 入口 |
接口与各个类的类图,如图1-1所示:
-
Observer接口
public interface Observer { /** * 1、观察者的抽象方法,作用是感知来自被观察对象的状态变化; * 2、即两个具体的观察者实现该方法后,可以得到来自被观察者的一些变化。 * @param abstractScoreGenerator */ void update(AbstractScoreGenerator abstractScoreGenerator); }
-
AbstractScoreGenerator抽象类
public abstract class AbstractScoreGenerator { /** * 获取分数的抽象方法 * @return 分数 */ public abstract int getScore(); /** * 生成分数的抽象方法 */ public abstract void execute(); /** * 初始化观察者Observer集合 */ private final ArrayList<Observer> observerList = new ArrayList<>(); /** * 添加/注册Observer观察者 * @param observer */ public void addObserver(Observer observer){ observerList.add(observer); } /** * 向Observer观察者发送通知 */ public void notifyObservers(){ for (Observer observer : observerList) { observer.update(this); } } }
-
ScoreGenerator功能实现类
public class ScoreGenerator extends AbstractScoreGenerator { /** * 随机数对象 */ private final Random random = new Random(); private int score; /** * 子类必须重写父类的抽象方法 * @return */ @Override public int getScore() { return score; } /** * 子类必须重写父类的抽象方法 * @return */ @Override public void execute() { for (int i = 0; i < 10; i++) { score = random.nextInt(20); this.notifyObservers(); } } }
-
DigitalObserver感知类
public class DigitalObserver implements Observer { /** * 具体的观察者,感知被观察者的变化,用数值的形式实现 * @param abstractScoreGenerator */ @Override public void update(AbstractScoreGenerator abstractScoreGenerator) { System.out.println("数值观察者:" + abstractScoreGenerator.getScore()); try { Thread.sleep(100); }catch (InterruptedException e) { throw new RuntimeException(e); } } }
-
GraphObserver感知类
public class GraphObserver implements Observer { /** * 具体的观察者,感知被观察者的变化,用图示的形式实现 * @param abstractScoreGenerator */ @Override public void update(AbstractScoreGenerator abstractScoreGenerator) { System.out.print("图示观察者:"); int count = abstractScoreGenerator.getScore(); for (int i = 0; i < count; i++) { System.out.print("*"); } System.out.println(""); try { Thread.sleep(100); }catch (InterruptedException e) { throw new RuntimeException(e); } } }
-
Main入口
public class Main { public static void main(String[] args) { // 抽象类不可以实例化,子类可以实例化,且由于继承关系,子类可以调用父类的普通方法 ScoreGenerator scoreGenerator = new ScoreGenerator(); // 接口不能实例化,但是可以通过实例化实现接口的类,从而”构造“该接口,并实现接口的抽象方法 DigitalObserver digitalObserver = new DigitalObserver(); GraphObserver graphObserver = new GraphObserver(); // 注册两个观察者(数值和图示),实际上就是成生成两个”接口对象“,目的是调用接口的具体实现 scoreGenerator.addObserver(digitalObserver); scoreGenerator.addObserver(graphObserver); // 计算分数结果:引入两个具体的观察者,将本方法计算的结果调用各自的具体实现 scoreGenerator.execute(); } }
运行结果(部分),如图1-2所示:
1.3模式要点
-
四种角色:观察者(接口)、具体观察者(接口实现类)、被观察者(抽象类),被观察者实现(子类)
-
利用抽象类和接口从具体的类中抽出抽象方法
-
将实例作为参数传递到类中,且不使用具体类型,而使用抽象类型或接口
-
观察者(Observer)不关心要观察的对象是谁,被观察者(Subject)也不关心是谁在观察自己,两者之间通过抽象类和接口产生关联
二、策略模式
2.1基本概念
策略模式,即Strategy模式:用不同的算法(方式)去解决同一个问题,并各个算法间得到替换,其主要目的是通过定义相似的算法,替换if-else写法。
2.2Demo案例
接下来同样还是使用一个简单的demo来介绍策略模式。
Java 在进行数值计算的时候,会经常用到加减乘除方法。如果我们想得到两个数字相加的和,我们需要用到“+”符号,得到相减的差,需要用到“-”符号等。
虽然我们可以通过字符串比较使用if-else写成通用方法,但是计算的符号每次增加,我们就不得不加在原先的方法中进行增加相应的代码,如果后续计算方法增加、修改或删除,那么会使后续的维护变得困难。
但是在这些方法中,我们发现其基本的加减乘除是固定的,这时我们就可以通过策略模式来进行开发,可以有效避免通过if-else来进行判断,即使后续增加其他的计算规则也可灵活进行调整。
类和接口一览,如表2.1所示:
名称 | 说明 |
---|---|
Strategy | 策略的对外接口,所有具体策略类都需实现该接口 |
CalculatorContext | 策略的上下文 |
OperationAddition | 一个策略的具体实现 |
OperationDivision | 一个策略的具体实现 |
OperationMultiplication | 一个策略的具体实现 |
Main | 入口 |
下面则是UML类图,如图2-1所示:
-
Strategy接口
public interface Strategy { /** * 策略的对外接口,所有具体策略类都需实现该接口 * @param num1 * @param num2 * @return */ int calculate(int num1, int num2); }
-
CalculatorContext 策略上下文类
public class CalculatorContext { private final Strategy strategy; /** * 接口作为类的属性,有参构方法造,构造策略上下文对象 * @param strategy */ public CalculatorContext(Strategy strategy){ this.strategy = strategy; } /** * 策略执行方法,调用后会直接实现对应的策略 * @param num1 * @param num2 * @return */ public int executeStrategy(int num1, int num2){ return strategy.calculate(num1,num2); } }
-
OperationAddition 策略的具体实现类(加法)
public class OperationAddition implements Strategy { /** * 表示加法的具体策略,两数相加 * @param num1 * @param num2 * @return */ @Override public int calculate(int num1, int num2) { return num1 + num2; } }
-
OperationDivision 策略的具体实现类(减法)
public class OperationDivision implements Strategy { /** * 表示减法的具体策略,两数相减 * @param num1 * @param num2 * @return */ @Override public int calculate(int num1, int num2) { return num1 - num2; } }
-
OperationMultiplication 策略的具体实现(乘法)
public class OperationMultiplication implements Strategy { /** * 表示乘法的具体策略,两数相乘 * @param num1 * @param num2 * @return */ @Override public int calculate(int num1, int num2) { return num1 * num2; } }
-
Main入口
public class Main { public static void main(String[] args) { // 初始化分值 int num1 = 6; int num2 = 8; // 实例化上下文对象,通过实例化实现接口的某个类去生成”接口对象“,目的是调用对应实现类的抽象方法实现 CalculatorContext calculatorContext1 = new CalculatorContext(new OperationAddition()); System.out.println("加法策略:num1+num2= " + calculatorContext1.executeStrategy(num1, num2)); // 通过调用每个不同实现类的不同抽象方法实现,可以解决if-else的逻辑判断 CalculatorContext calculatorContext2 = new CalculatorContext(new OperationDivision()); System.out.println("减法策略:num1-num2= " + calculatorContext2.executeStrategy(num1, num2)); // 策略模式最核心的:客户端可以通过实例化不同的”接口对象“去调用不同的具体实现 CalculatorContext calculatorContext3 = new CalculatorContext(new OperationMultiplication()); System.out.println("乘法策略:num1*num2= " + calculatorContext3.executeStrategy(num1, num2)); } }
运行结果,如图2-2所示:
2.3模式角色
主要由这3个角色组成:
- 策略上下文角色(Context),提供给客户端使用,持有该类的一个策略对象的引用,核心就是串联策略接口与其具体实现。
- 抽象策略角色(Strategy):这是一个抽象角色,通常是接口或者抽象类,给出了所有具体策略类所需的接口。
- 具体策略角色:封装了一些具体的实现方法(行为)或算法。
2.4模式要点
-
为什么需要抽象策略角色?
通常我们在开发的时候,具体的行为会写在方法中,而策略模式却将算法与其它部分分离开,只是暴露了算法的API接口,然后在需要使用的地方以委托的方式来使用。
这样看起来代码好像变复杂了,有种脱裤子放屁的嫌疑。其实不然,当业务上有调整需要新增、修改这些算法时,我们只需要选择性地调用即可,就不必再修改策略角色了。最重要的是,利用委托这种弱关联关系可以方便地整体替换算法。
三、建造者模式
大都市中林立着许多高楼大厦,这些高楼大厦都是具有建筑结构的大型建筑,在英文中通常把这些建筑称为 Builder。
建造这些庞然大物时,一般难以一气呵成。此时我们需要先建造组成这个物体的各个部分,然后再分阶段将它们组装起来。
在编写代码时,我们肯定遇到过新建对象、并为对象的属性赋值的场景。下面就介绍 Builder 建造者模式在创建对象并赋值时的作用。
3.1传统的Builder模式
首先介绍的是更为抽象的传统Builder模式,其核心思想在于:分阶段组装具有复杂结构的实例,并隐藏具体的组装过程,本质还是一种调用抽象方法具体实现的思想。
需要借助的角色有4个:需要被Builder的类、抽象的Builder类、具体实现Builder的类、调用具体实现的Director。如表3.1所示:
名称 | 说明 |
---|---|
Computer | 一个需要被建造的实体类,其属性就是这个“建筑的一部分” |
ComputerBuilder | 一个抽象类,定义了建造属性的抽象方法 |
ConcreteComputerBuilder | 一个具体的实现类,实质上就是使用setter方法为属性赋值 |
Director | 监督者,实质上就是调用方 |
Main | 入口 |
下面是对应的UML图,如图3-1所示:
- Computer类
@Data
public class Computer {
/**
* 中央处理器
*/
private String cpu;
/**
* 内存
*/
private String memory;
/**
* 硬盘
*/
private String disk;
}
- ComputerBuilder建造抽象类
public abstract class ComputerBuilder {
// 创建产品对象
protected Computer computer = new Computer();
// 创建产品对象的各组成部件,即设置对象的属性
public abstract void setCpu();
public abstract void setMemory();
public abstract void setDisk();
// 返回产品对象
public Computer getComputer(){
return computer;
}
}
- ConcreteComputerBuilder具体的建造过程
public class ConcreteComputerBuilder extends ComputerBuilder {
@Override
public void setCpu() {
computer.setCpu("i5-7500");
}
@Override
public void setMemory() {
computer.setMemory("16GB");
}
@Override
public void setDisk() {
computer.setDisk("500GB");
}
}
- Director监督者调用
public class Director {
private final ComputerBuilder computerBuilder;
/**
* 有参构造
* @param builder
*/
public Director(ComputerBuilder computerBuilder) {
this.computerBuilder = computerBuilder;
}
/**
* 调用抽象方法的具体实现
* @return
*/
public Computer computerConstruct() {
// 调用 builder 的属性设置方法
computerBuilder.setCpu();
computerBuilder.setMemory();
computerBuilder.setDisk();
// 返回组装好的电脑
return computerBuilder.getComputer();
}
}
- Main主类
public class Main {
public static void main(String[] args) {
ConcreteComputerBuilder builder = new ConcreteComputerBuilder();
Director director = new Director(builder);
Computer computer = director.computerConstruct();
// 查看电脑信息
System.out.println("cpu型号:"+computer.getCpu()+"," +"内存大小:"+computer.getMemory()+ ","+"硬盘大小:"+ computer.getDisk());
}
}
运行结果,如图3-2所示:
而改良后的Builder模式,只需要两步就可以实现建造一个类并为其赋值的过程,同时还可以灵活地调整顺序,也可以只建造一部分。
具体分为两个部分:1、添加@Builder注解;2、链式调用建造。
- 添加@Builder注解
@Builder
public class Computer {
/**
* 中央处理器
*/
private String cpu;
/**
* 内存
*/
private String memory;
/**
* 硬盘
*/
private String disk;
}
- 链式调用建造
public class Main {
public static void main(String[] args) {
Computer computer = Computer.builder()
.cpu("R7-6800H")
.memory("SAMSUNG 32GB")
.disk("SSD 1TB")
.build();
// 查看电脑信息
System.out.println("cpu型号:" + computer.getCpu() + ","
+ "内存大小:"+ computer.getMemory() + ","
+ "硬盘大小:"+ computer.getDisk());
}
}
运行结果,如图3-3所示:
3.3模式要点
先说说改良版的优点:
- 对象的创建过程更加灵活,可以选择性的初始化对象的某些属性,而非所有属性;
- 相对通过构造函数创建对象,代码可读性更高:能将属性和所赋的值关联起来,也能清晰地知道对象的内容。
再说缺点(不算缺点的缺点):
- 一旦需要建造的对象属性有变化,在链式调用的地方也需要同步修改。
传统版的建造者模式,在许多优秀的开源框架中有大量地应用,由于本人的水平、认识有限,在实际的项目中很少使用到这样的思想去创建对象并赋值。
但不乏举出两个显而易见的优点:
- 封装性好,实现了对象的创建与表示分离;
- 扩展性好,具体建造者之间相互独立,有利于系统的解耦。
四、外观(门面)模式
随着时间的推移、需求的增加,程序的结构很有可能会越来越大,子系统可能会越来越多。
我们可以为项目或者程序准备一个对外的”窗口“,这样用户不需要关注每个类和接口之间的联系,只需简单地对这个窗口提出请求即可。
4.1基本概念
facade 来源于法语单词,原意为”建筑物的正面“,使用 facade 模式,其中的 facade 角色可以让整个系统对外只有一个简单的API,并且还会考虑到系统内部各个类之间的依赖关系,同时按照正确的顺序调用各个类。
4.2Demo案例
下面以一个博客系统项目的设计为 demo 举例,博客系统里主要包括博客(文章)后台,APP端(H5)。
其中后台又具体包括:博客编辑(用户)、博客审核(管理员)、数据统计(点赞&收藏)、用户列表等模块;
下面以博客编辑为例,看看 facade 模式在这样的系统中扮演了什么角色,又起到了什么作用。
类和接口一览,如表4.1所示:
名称 | 说明 |
---|---|
BlogController | 与前端交互的接口暴露,Restful-API 风格 |
BlogFacade | facade 角色,BlogController 直接引用该类的对象 |
BlogService | 抽象方法的集合 |
BlogServiceImpl | 抽象方法的具体实现 |
BlogMapper | 操作 MySQL 的封装框架 |
Main | 程序入口 |
在写具体的代码之前,我们可以先梳理一下项目的基本结构,如图4-1所示,除了经典的 controller、service、model、mapper 外,还有一个 facade 层。
大家可以重点观察一下,看新加了 facade 后,系统内部各个类、接口之间的调用是怎么样的关系。如图4-1所示:
- BlogController
@RestController
@RequestMapping("/Blog")
@Api(value = "博客接口", tags = "Blog")
public class BlogController {
@Resource
private BlogFacade blogFacade;
@ApiOperation(value = "新增博客", httpMethod = "POST", response = Boolean.class)
@PostMapping("/createBlog")
public ResponseData createBlog(@RequestBody CreateBlogDto dto){
Boolean boolean = blogFacade.createBlog(dto);
return ResponseData.success(boolean);
}
@ApiOperation(value = "编辑博客", httpMethod = "POST", response = Boolean.class)
@PostMapping("/updateBlog")
public ResponseData updateBlog(@RequestBody UpdateBlogDto dto){
Boolean boolean = blogFacade.updateBlog(dto);
return ResponseData.success(boolean);
}
}
- BlogFacade
@Service
public class BlogFacade {
/**
* 注入interface
*/
@Resource
private BlogService blogService;
/**
* 注入interface
*/
@Resource
private BlogLikeService blogLikeService;
/**
* 具体实现
*/
@Transactional(rollbackFor = Exception.class)
@SneakyThrows
public Boolean createBlog(CreateBlogDto dto){
Blog blog = new Blog();
BeanUtils.copyProperties(dto, blog);
blog.setCreateTime(new Date());
Integer blogId = blogService.saveBlog(blog);
}
@Transactional(rollbackFor = Exception.class)
@SneakyThrows
public Boolean updateBlog(UpdateBlogDto dto){
// 具体实现
// 略
}
}
与经典的在Controller类中注入service不同,这里引入的是facade类,在facade类中再去注入service的接口,整个facade类中做的是Controller中所需要的具体实现。
- BlogService
public interface BlogService extends IService<Blog> {
/**
* 新建博客
* @param blog
* @return 是否成功
*/
Integer saveBlog(Blog blog);
}
- BlogServiceImpl
@Service
public class BlogServiceImpl extends ServiceImpl<BlogMapper, Blog> implements BlogService {
@Resource
private BlogMapper blogMapper;
/**
* 新建博客
* @param blog
* @return
*/
@Override
public Integer saveBlog(Blog blog) {
this.save(essayManage);;
return blog.getId();
}
而service层中的impl实现类,只做与数据库相关的操作。
- BlogMapper
@Mapper
public interface BlogMapper extends BaseMapper<Blog> {
}
有的同学可能已经发现了,facade 好像只是把原来在 impl 层做的逻辑实现,放到了 facade 层里,但仍然还是要实现 service 接口的抽象方法,那还有必要再分一个facade 层出来吗?
答案:在整个系统复杂且子系统多的时候,比较适合使用 facade 模式。
4.3模式要点
facade 模式出现的角色比较简单,分为3个:facade 角色、系统其它角色(类或接口等)、请求方。
- facade 角色:抽象角色,负责将请求中转(转发)给子系统处理
- 系统其它角色:功能的具体实现,目的是完成内部封装,只返回数据或结果给API;
- 请求方:其实就是项目启动后,调用接口的用户,或者说是对外暴露可供调用的API;
由以上分析可知,Facade设计模式更注重从构架的层次去看整个系统,而不是单个类、接口的层次,对于各个子系统的解耦很有帮助。
解耦的重点在于:起码从直观上可以很明显地发现——interface变少了,这里的API指的是系统内部的接口,而非暴露给外部的Restful-API。
在开发的时候,如果有这么一种模式:能让接口变少的同时,还能让我们专注逻辑实现,且可以方便地对外暴露请求的 facade 角色,该是多么地美好!
五、适配器模式
5.1基本概念
常见的设计模式之一,其最核心的思想:在不改变现有系统结构的情况下,将一个类的接口转换成用户希望的另一个接口。
5.2Demo案例
下面介绍两种不同实现,这两种都是很经典的适配器模式实现。
5.2.1类适配器
本质是通过继承的方式来实现接口的适配的,具体看代码大家就明白了,这个继承的妙处到底在哪里。
背景简述:我到香港迪士尼去游玩,晚上在酒店想给笔记本充电,但我发现香港的插座是英式三角插座,我的充电器插不进去,这个时候就可以使用适配器模式进行适配。
类与接口的关系如表5.1所示:
名称 | 说明 |
---|---|
BritishStandard | 已经存在的角色接口,是面向用户的、最终使用的接口 |
ChineseStandard | 也是已存在的角儿,是需要被转换(被适配)的接口 |
StandardAdapter | 新的角色,也是核心角色,作用是转换接口 |
Main | 程序入口 |
对应的UML图所图5-1所示:
- BritishStandard接口
public interface BritishStandard {
/**
* 目标角色,用户只适配这个接口
* @return
*/
String getBritishStandard();
}
- ChineseStandard类
public class ChineseStandard {
/**
* 已存在的角色,但是需要被转换才能被用户使用
* @return
*/
public String getChineseStandard() {
return "中式插座";
}
}
- StandardAdapter适配器类
public class StandardAdapter extends ChineseStandard implements BritishStandard {
/**
* 实质是通过继承,将源方法放入目标方法中
* @return
*/
@Override
public String getBritishStandard() {
return this.getChineseStandard();
}
}
- 启动入口(笔记本)
public class Notebook {
public Boolean charge(BritishStandard britishStandard) {
if ("中式插座".equals(britishStandard.getBritishStandard())) {
System.out.println("充电成功!");
return Boolean.TRUE;
} else {
throw new BusinessException("充电失败!");
}
}
public static void main(String[] args) {
// 通过实例化实现接口的类来传递"接口对象"
Boolean result = new Notebook().charge(new StandardAdapter());
Assert.isTrue(result,"适配失败!请重试");
}
}
运行结果如图5-2所示:
5.2.2对象适配器
本质上是通过构造器传递(委托)的方式来实现适配的,具体看代码:
背景简述:我的车有车载音乐播放系统,一个是播放数字音乐的接口MusicPlayer,另一个是播放CD光盘的接口CdPlayer。而我想要将CD光盘中我喜欢的音乐转化成 mp3 的数字音乐格式来播放。这个时候就可以使用适配器模式进行适配。
类与接口的关系如表5.2所示:
名称 | 说明 |
---|---|
MusicPlayer | 已经存在的角色接口,是面向用户的、最终使用的接口 |
CdPlayer | 也是已存在的角儿,是需要被转换(被适配)的接口 |
PlayerAdapter | 新的角色,也是核心角色,作用是转换接口 |
Main | 程序入口 |
对应的UML图所图5-3所示:
- MusicPlayer接口
public interface MusicPlayer {
/**
* 已存在的角色,用户只能使用这个接口
* @param fileName
*/
String playMusic(String fileName);
}
- CdPlayer类
public class CdPlayer {
/**
* 已存在的角色,需要被转换才能使用
* @param fileName
* @return
*/
String playCD(String fileName){
return "播放CD歌曲"+ fileName +"成功!";
}
}
- PlayerAdapter适配器类
@AllArgsConstructor
public class PlayerAdapter implements MusicPlayer{
@Resource
private CdPlayer cdPlayer;
/**
* 使用有参构造的方式,传递对象
* @param fileName
* @return
*/
@Override
public String playMusic(String fileName) {
return cdPlayer.playCD(fileName);
}
}
- 入口(播放音乐)
public class Play {
public static void main(String[] args) {
PlayerAdapter playerAdapter = new PlayerAdapter(new CdPlayer());
String result = playerAdapter.playMusic("《韩宝仪-往事只能回味》");
System.out.println(result);
Assert.hasLength(result, "播放失败,请重试!");
}
}
运行结果如图5-4所示:
4.3模式要点
出现的3种角色:
- 目标角色(Target):已经存在的角色,是用户最终需要的接口;
- 源角色(Adaptee):需要被转换的接口,也是已经存在的角色;
- 适配器角色(Adapter):核心角色,通过继承或者类关联的方式将源角色转换为目标角色。
优缺点分析:
类适配器
-
优点:可以根据需求重写 Target 的方法,使得 Adapter 的灵活性增强了。
-
缺点:有一定局限性。因为类适配器需要继承 Adaptee 类,而 Java 是单继承机制,所以要求 Adaptee 必须是一个类。
对象适配器
-
优点:同一个 Adapter 可以把 Adaptee 类和他的子类都适配到目标接口。
-
缺点:需要重新定义 Adaptee 行为时,需要重新定义 Adaptee 的子类,并将适配器组合适配。
文章小结
到这里5种常用的设计模式就和大家分享完了,熟练使用设计模式可以提高我们的代码质量,且能使得我们的程序设计地更优雅,也更易读易懂。
由于本人水平有限,对于文章有问题的地方欢迎大家指正,不吝赐教,有其它想法也可以在评论区一起交流学习。
参考文献
- 《图解设计模式》【日】结城浩 著,杨文轩 译,中国工信出版集团,人民邮电出版社;
- https://www.cnblogs.com/xuwujing/p/9954263.html#5195605
- https://blog.csdn.net/u014454538/article/details/122377789
- https://blog.csdn.net/qq_36566262/article/details/124242610
- https://blog.csdn.net/weixin_51466332/article/details/123345199