一、前言
有时候不想动脑子,就懒得看源码又不像浪费时间所以会看看书,但是又记不住,所以决定开始写"抄书"系列。本系列大部分内容都是来源于《 图解设计模式》(【日】结城浩 著)。该系列文章可随意转载。
二、Observer 模式
Observer 模式 : 发送状态变化通知
1. 介绍
Observer 即 “观察者”,在 Observer 模式中,当观察对象的状态发生变化时,会通知给观察者。 Observer 模式适用于根据对象状态进行相应处理的场景。
Observer 模式 中出场的角色:
- Subject (观察对象):Subject 角色表示观察对象。Subject 角色定义了注册观察者和删除观察者的方法。此外,他还声明了 “获取现在的状态” 的方法。
- ConcreteSubject (具体的观察对象):ConcreteSubject 角色表示具体的被观察的对象。当自身状态发生变化后,他会通知所有已经注册的 Observer 角色。
- Observer(观察者):Observer 角色负责接收来着Subject 角色的状态变化的通知。为此,它声明了 update 方法。
- ConcreteObserver(具体的观察者):ConcreteObserver 角色负责具体的 Observer。当他的 update 方法被调用后,会去获取要观察的对象的最新状态。
类图如下:
Demo 如下
// 监听者接口
public interface Observer {
void update(NumberGenerator generator);
}
// 监听者实现类
public class NumberObserver implements Observer{
@Override
public void update(NumberGenerator generator) {
System.out.println("number = " + generator.getNumber());
}
}
// 数字生成器
public class NumberGenerator {
private List<Observer> observers = Lists.newArrayList();
@Getter
private int number;
/**
* 执行某个操作
*/
public void execute() {
Random random = new Random(System.currentTimeMillis());
for (int i = 0; i < 20; i++) {
number = random.nextInt();
// 通知监听者动作执行
notifyObservers();
}
}
// 添加监听者
public void addObserver(Observer observer) {
observers.add(observer);
}
/**
* 通知所有监听者
*/
public void notifyObservers() {
observers.forEach(observer -> observer.update(this));
}
}
public class ObserverDemoMain {
public static void main(String[] args) {
final NumberGenerator generator = new NumberGenerator();
NumberObserver observer = new NumberObserver();
// 添加监听者
generator.addObserver(observer);
// 执行某个操作
generator.execute();
}
}
2. 应用
-
诸如 MQ 中的消费者也是订阅了相关的 Queue,当相关的 Queue有消息进入后,会推送给消费者得知,消费者可以处理对应消息。
-
Spring 中向事务管理器TransactionSynchronizationManager注册事务某个阶段执行的监听事件,如下:
@Transactional(rollbackFor = Exception.class) @Override public String testTransactionSynchronization() { // step1 : 在 sys_role 表中插入一条 role_id = 1 的记录 addRole(1); // step2 : 在 sys_role 表中插入一条 role_id = 2 的记录 addRole(2); // 向事务同步管理器注册一个 TransactionSynchronization 匿名实现类,作用是当前事务提交后,执行 addPermission 方法。 TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronizationAdapter() { // 当前事务提交后触发该方法 // step3 : 在 sys_rermission 表中插入一条 permission_id = id 的记录。 @Override public void afterCommit() { addPermission(1); } }); System.out.println("end"); return ""; }
-
Spring 中典型的监听器模式:即我们通过自定义 ApplicationEvent 同时通过实现 ApplicationListener 来监听指定事件的发生。如下,在 SpringBoot 启动时会调用如下几个方法,发送容器初始化、容器初始化结束等相关消息,监听对应事件的监听器会监听到该消息并可以做特定的处理。
个人使用:该部分内容是写给自己看的,帮助自身理解,因此就不交代项目背景了,读者请自行忽略(◐ˍ◑):
- 项目 A 中,对于订单状态的刷新可以有两个触发点:用户点击刷新或接收到 MQ 推送消息后刷新。因此这里做了一个统一,无论是 MQ 还是用户点击刷新,都会触发一个 RefreshEvent 事件,而存在一个 RefreshEvent 事件的监听器来监听该事件。当事件触发时会调用监听器的指定方法来完成刷新操作。
3. 总结
扩展思路
- 可替换性:使用设计模式的目的之一就是一使类成为可复用的组件。在 Observer 模式中,有连接的仅仅是 Subject 角色 和 Observer 角色,而 ConcreteSubject 和 ConcreteObserver 角色并没有直接联系,可以随意替换。
- 当 Observer 的行为会对 Subject 产生影响时 :当 Subject 触发 update 操作时会调用 Observer 方法,但是需要注意如果 Observer 角色中如果再调用 Subject 的 update 方法可能就会造成循环调用。
- 传递更新信息的方式:有时我们可以在 Subject 触发时将需要更新的信息传递过去,这样就省去了 Observer 自己获取数据的麻烦,不过如此Subject 就知道了 Observer 中要进行的操作了,在复杂程序中这样的操作会使程序缺少灵活性,所以需要根据程序的复杂度来考虑在触发时数据传递的方式。
- 从“观察”变为“通知”:Observer 本意是 观察者,但实际上 Observer 角色并非主动的去观察,而是被动的接受来自 Subject 角色的通知。因此 Observer 模式也被称为 Publish-Subscribe (发布-订阅)模式。
相关设计模式:
-
Mediator 模式:在 Mediator 模式中,有时会使用 Observer 模式来实现 Mediator 角色与 Colleague 角色之间的通信。就“发送状态变化通知”这一点而已,Mediator 模式与 Observer 模式是类似的。不过这两种模式中通知的目的和视角不同。
在Mediator 模式中,虽然也会发送通知,不过那只是为了对 Colleague 角色进行仲裁而已,而在 Observer 模式中,将 Subject 角色的状态变化通知给 Observer 角色的目的则主要是为了使 Subject 角色与 Observer 角色同步。
三、Memento 模式
Memento 模式 :保存对象状态
1. 介绍
我们在文本编辑器编辑时,如果不小心误操作,可以通过撤回(undo)功能将内容恢复到之前的状态。而面对对象编程的方式实现撤销功能时,需要事先保存实例的相关状态信息,然后再撤销时,还需要根据所保存的信息将实例恢复到原先的状态。
想要恢复实例,需要一个可以自由访问实例内部结构的权限,但是如果稍微不注意又可能会将依赖于实例内部结构的代码分散到编写在程序的各个地方,导致程序变的难以维护。这种情况叫做“破坏了封装性”。
而通过引入表示实例状态的角色,可以在保存和回复实例时有效地方志对象的封装性遭到破坏,即 Memento 模式。
Memento 模式 中出场的角色:
-
Originator(生成者): Originator角色会在保存自己的最新状态时生成 Memento角色。当把以前保存的 Memento 角色传递给 Originator 角色时,他会将自己恢复至生成该Memento角色时的状态。
-
Memento(纪念品):Memento 角色会将 Originator 角色的内部信息整合在一起,在Memento角色中虽然保存了 Originator 角色的信息,但是他不会向外部公开这些信息。Memento 角色有以下两种接口:
- wide interface(宽接口):Memento 角色提供的宽接口是指所有用于获取回复对象状态信息的方法的集合。由于宽接口会暴露所有 Memento 角色的内部信息,因此能够使用宽接口的只有 Originator 角色。
- narrow interface (窄接口) :Memento角色为外部的 Caretaker 角色提供了窄接口。可以通过窄接口获取的 Memento 角色的内部信息非常有限,因此可以有效防止信息泄漏。
通过对外提供这两种接口,可由有效防止对象的封装性被破坏。
-
Caretaker(负责人):当Caretaker 角色想要保存当前的 Originator 角色状态时,会通过 Originator 角色。Originator角色在接收到通知后会生成 Memento 角色的实力并将其返回给 Caretaker 角色。由于以后可能会用 Memento 实例来将 Originator 恢复至原来的状态,所以 Caretaker 角色会一直保存Memento 实例。
不过由于 Caretaker 角色只能使用Memento 角色的两种接口中的窄接口,也就是说它无法访问 Memento 角色内部的所有信息。它只是将 Originator 角色生成的Memento角色当做一个黑盒保存起来。
虽然 Originator 角色和 Memento 角色之前是强关联,但是Caretaker 角色和 Memento 角色之前是弱关联关系。Memento 角色对 Caretaker 角色因此了自身的内部信息。
类图如下:
Demo 如下: 游戏自动进行,通过掷骰子来决定下一个状态,点数为1时增加金钱,2时减少,6时得到水果,而如果金钱减少则通过 Memento 对象回滚到上一轮的状态。
public class Memento {
/**
* 当前持有金钱
*/
private final int money;
/**
* 当前持有水果
*/
private final List<String> fruits = Lists.newArrayList();
public Memento(int money) {
this.money = money;
}
/**
* 获取当前金钱
* @return
*/
public int getMoney() {
return money;
}
/**
* 添加水果
* @param fruit
*/
public void addFruit(String fruit){
fruits.add(fruit);
}
/**
* 获取持有水果
* @return
*/
public List<String> getFruits(){
return fruits;
}
}
public class Gamer {
/**
* 当前持有金钱
*/
private int money;
/**
* 当前持有水鬼
*/
private List<String> fruits = Lists.newArrayList();
/**
* 水果列表
*/
private String[] fruitsName = new String[]{"苹果", "橘子", "香蕉"};
/**
* 随机数
*/
private Random random = new Random(System.currentTimeMillis());
public Gamer(int money) {
this.money = money;
}
public int getMoney() {
return money;
}
public List<String> getFruits() {
return fruits;
}
public void bet() {
final int dice = random.nextInt(6) + 1;
if (dice == 1) {
money += 100;
System.out.println("所持有的金钱增加了");
} else if (dice == 2) {
money /= 2;
System.out.println("所持有的金钱减半了");
} else if (dice == 6) {
final String fruit = getFruit();
fruits.add(fruit);
System.out.println("获得了水果 " + fruit);
} else {
System.out.println("什么都没发生");
}
}
/**
* 创建快照
*
* @return
*/
public Memento createMemento() {
Memento memento = new Memento(money);
fruits.stream()
.filter(f -> f.startsWith("好吃的"))
.forEach(memento::addFruit);
return memento;
}
/**
* 快照还原
* @param memento
*/
public void restoreMemento(Memento memento) {
this.money = memento.getMoney();
this.fruits = memento.getFruits();
}
/**
* 获取奖励水果
*
* @return
*/
private String getFruit() {
String prefix = random.nextBoolean() ? "好吃的" : "";
return prefix + fruitsName[random.nextInt(fruitsName.length)];
}
}
public class MementoDemoMain {
public static void main(String[] args) {
Gamer gamer = new Gamer(100);
Memento memento = gamer.createMemento();
for (int i = 0; i < 100; i++) {
System.out.println("i = " + i + ": 当前金钱 = " + gamer.getMoney() + "; 当前水果 = " + gamer.getFruits());
gamer.bet();
System.out.println("当前持有金钱 money = " + gamer.getMoney());
if (gamer.getMoney() > memento.getMoney()) {
System.out.println("当前持有金钱增加, 继续游戏");
memento = gamer.createMemento();
} else {
System.out.println("当前持有金钱减少, 恢复快照状态");
gamer.restoreMemento(memento);
}
}
}
}
输出如下:
2. 应用
- 想到了数据库中的事务回滚,效果表现与 Memento 模式有些类似,表象为事务执行失败后将数据回滚到事务开启前的快照版本,但是这并不是简单的回滚快照,而是通过 Undo 版本链(如有需要可参阅 https://blog.csdn.net/filling_l/article/details/112854716)来实现的,因此不能直接带入 State 模式,
只是突然想到了而已。 - 除此之外又突然想到了 MySQL 中的 SavePoint 的内容(与本文无关,想到了就记录下):即 MySQL 中的事务中存在一个 SavePoint 的概念:即一个事务中可以创建一个或多个 SavePoint,当设置SavePoint 时 当事务只会回滚到 SavePoint,而 SavePoint 之前的内容不会回滚。如一个事务中,先增加两条记录,然后设置一个 SavePoint, 再删除两条记录,此时出现了某种异常,事务进行回滚时则只会回滚删除语句的操作,插入语句则不会回滚。
个人使用:该部分内容是写给自己看的,帮助自身理解,因此就不交代项目背景了,读者请自行忽略(◐ˍ◑):
- 在项目A中对单证会进行多次修改并保存,而相关的操作人员经常会不小心修改某个参数并保存,而在后续过程中会被其他人员发现数据错误,而修改后操作人员并不承认自己曾经修改过数据。因此可以通过 Memento 模式记录人员的每次修改内容,以便追溯修改人员以及数据回滚。
3. 总结
扩展思路
- 需要多少个 Memento :根据实际业务的不同,可以保存多个 Memento 实例,就可以是实现保存的各个时间点的对象的状态。
- Memento 的有效期限是多久:如果将 Memento 永远保存在文件中,就可能会出现有效期限的问题。因为可能在后续的某个时间点将 Memento 保存在文件中之后升级了程序版本,导致保存的 Memento 与当前程勋内部不匹配。
- 划分 Caretaker 角色 和 Originator 角色的意义:如果要实现撤销功能,为什么不直接在 Originator 角色中实现?这是因为 Caretaker 角色的职责是觉得何时拍摄快照、何时撤销以及保存 Memento 角色。另一方面 Originator 角色的职责是生成 Memento 角色和使用接收到的 Memento 角色来恢复自己的状态。以上是 Caretaker 角色和 Originator 角色的职责分担,有了这样的职责分担,当我们需要对应一下需求变更时,就可以完全不修改 Originator 角色。
- 变更为可以多次撤销。
- 变更为不仅可以撤销,还可以将现在的状态保存在文件中。
相关设计模式
- Command 模式:在使用 Command 模式处理命令是,可以使用 Memento 模式实现撤销功能。
- Protype 模式:在 Memento 模式中,为了能够实现快照和撤销功能,保存了对象当前的状态。保存的信息只是在恢复状态时所需要的那部分信息。而在 Protype 模式中,会生成一个与当前实例完全相同的另外一个实例。这两个实例的内容完全一样。
- State 模式:在 Memento 模式中,是用实例来表示状态,而在 State 模式中,则是用类表示状态。
一时的小想法,仅仅个人理解,无需在意 :
- 在实际保存快照的过程中,要考虑到保存时效的问题,时效过长则会占用空间并且可能会跟不上版本。另外还需要考虑到存储的方式,如果快照内容非常大,就不适合存到数据库中,因为快照一般都不需要搜索,所以可以将内容写入到文件中,数据库中保存文件地址即可。
四、State 模式
State 模式 :用类表示状态
1. 介绍
在面向对象编程中,是用类表示对象的。而在 State 模式中,我们可以用类来表示状态。
State 模式 中出场的角色:
- State (状态): State 角色表示状态,定义了根据不同状态进行不同处理的接口。该接口是那些处理内容依赖于状态的方法的集合。
- ConcreteState(具体状态):ConcreteState角色表示各个具体的状态,它实现了State接口。
- Context(状况、前后关系、上下文):Context 角色持有当前状态的 ConcreteState 角色。此外它还定义了供外部调用者调用 State 模式的接口。
类图如下
Demo如下 : 一个金库系统,分为白天和晚上两个状态,每小时使用一次金库,同步一次警局。
// 状态接口
public interface State {
/**
* 设置时间
* @param context
* @param hour
*/
void doClock(Context context, int hour);
/**
* 使用金库
* @param context
*/
void doUse(Context context);
/**
* 正常通话
* @param context
*/
void doPhone(Context context);
}
// 白天状态
public class DayState implements State {
private static final DayState INSTANCE = new DayState();
private DayState() {
}
/**
* 单例模式获取实例
*
* @return
*/
public static DayState getInstance() {
return INSTANCE;
}
@Override
public void doClock(Context context, int hour) {
if (hour < 9 || hour >= 17) {
context.changeState(NightState.getInstance());
}
}
@Override
public void doUse(Context context) {
System.out.println("白天使用金库");
}
@Override
public void doPhone(Context context) {
System.out.println("白天正常通话");
}
@Override
public String toString() {
return "白天";
}
}
// 晚上状态
public class NightState implements State {
private static final NightState INSTANCE = new NightState();
private NightState() {
}
/**
* 单例模式获取实例
*
* @return
*/
public static NightState getInstance() {
return INSTANCE;
}
@Override
public void doClock(Context context, int hour) {
if (hour >= 9 && hour < 17) {
context.changeState(DayState.getInstance());
}
}
@Override
public void doUse(Context context) {
System.out.println("晚上使用金库");
}
@Override
public void doPhone(Context context) {
System.out.println("晚上通话录音");
}
@Override
public String toString() {
return "晚上";
}
}
// 上下文
public class Context {
private State state;
public Context(State state) {
this.state = state;
}
/**
* 设置时间
*
* @param hour
*/
public void setClock(int hour) {
System.out.println("现在时间是 : " + hour + ":00");
state.doClock(this, hour);
}
/**
* 改变状态
*
* @param state
*/
public void changeState(State state) {
System.out.println("状态变更 : " + this.state + " -> " + state);
this.state = state;
}
/**
* 使用金库
*
* @param msg
*/
public void doUse(String msg) {
this.state.doUse(this);
}
/**
* 联系报警中心
*
* @param msg
*/
public void callSecurityCenter(String msg) {
this.state.doPhone(this);
}
}
// Main 方法
public class StateDemoMain {
public static void main(String[] args) throws InterruptedException {
// 执行状态,每小时使用一次金库, 拨打警局电话确认安全
Context context = new Context(DayState.getInstance());
for (int i = 0; i < 24; i++) {
context.setClock(i);
Thread.sleep(1000);
context.doUse("");
context.callSecurityCenter(String.valueOf(i));
}
}
}
输出如下:
2. 应用
没想到
个人使用:该部分内容是写给自己看的,帮助自身理解,因此就不交代项目背景了,读者请自行忽略(◐ˍ◑):
-
在 Memento 模式中举例的项目A中,实际上对于单证的处理不仅仅时修改,还存在多个环节状态的流转,而每个环节状态并非单独字段的判断,因此可以通过 State 模式来管控各个状态之间的流转,并且结合 Memento 模式,可以对每个环节状态做一个快照备份。
@Data public class BillInfo { /** * 表头信息 */ private String head; /** * 表体信息 */ private List<String> details; @Override public String toString() { return "BillInfo{" + "head='" + head + '\'' + ", details=" + details + '}'; } } public abstract class BillState { protected static final MementoContext MEMENTO_CONTEXT = new MementoContext(); /** * 编辑操作 * * @param billInfo */ public void edit(BillInfo billInfo) { throw new RuntimeException("not support this operation"); } /** * 发送操作 * * @param billInfo */ public void send(BillInfo billInfo) { throw new RuntimeException("not support this operation"); } /** * 删除操作 * * @param billInfo */ public void delete(BillInfo billInfo) { throw new RuntimeException("not support this operation"); } } @Data public class BillMemento { /** * 表头信息 */ private String head; /** * 表体信息 */ private List<String> details; /** * 创建时间 */ private Date createTime = new Date(); /** * 快照版本号 */ private String version; /** * 上一个版本号 */ private String lastVersion; } public class DeleteBillState extends BillState { @Override public void delete(BillInfo billInfo) { System.out.println("单证状态已被删除"); } } public class SendBillState extends BillState { @Override public void send(BillInfo billInfo) { System.out.println("单证已发送"); } @Override public void delete(BillInfo billInfo) { System.out.println("单证状态无法删除"); } } public class EditBillState extends BillState { @Override public void edit(BillInfo billInfo) { System.out.println("单证被编辑"); } @Override public void delete(BillInfo billInfo) { System.out.println("单证被删除"); } } public class BillStateProxy { private static final BillState EDIT_STATE = new EditBillState(); private static final BillState DELETE_STATE = new DeleteBillState(); private static final BillState SEND_STATE = new SendBillState(); /** * 当前状态 */ private BillState billState = EDIT_STATE; /** * 编辑操作 * * @param billInfo */ public String edit(BillInfo billInfo, String version) { billState.edit(billInfo); return MEMENTO_CONTEXT.createMemento(version, billInfo); } /** * 发送操作 * * @param billInfo */ public void send(BillInfo billInfo) { billState = SEND_STATE; billState.send(billInfo); } /** * 删除操作 * * @param billInfo */ public void delete(BillInfo billInfo) { billState = DELETE_STATE; billState.delete(billInfo); } /** * 快照恢复 * @param billInfo * @param version */ public void restoreBillInfo(BillInfo billInfo, String version){ MEMENTO_CONTEXT.restoreMemento(billInfo, version); } } public class MementoContext { /** * 快照存储 */ private Map<String, BillMemento> store = Maps.newHashMap(); /** * 创建快照 * * @param billInfo * @return */ public String createMemento(String laseVersion, BillInfo billInfo) { BillMemento memento = new BillMemento(); memento.setHead(billInfo.getHead()); memento.setDetails(Lists.newArrayList(billInfo.getDetails())); memento.setVersion(UUID.randomUUID().toString()); memento.setLastVersion(laseVersion); storeMemento(memento); return memento.getVersion(); } /** * 快照回滚 * * @param billInfo * @param version */ public void restoreMemento(BillInfo billInfo, String version) { final BillMemento memento = getMemento(version); billInfo.setHead(memento.getHead()); billInfo.setDetails(memento.getDetails()); } /** * 存储快照, 可以重写该方法以改变快照的存储方式 * * @param memento */ protected void storeMemento(BillMemento memento) { store.put(memento.getVersion(), memento); } /** * 获取快照 * * @param version */ protected BillMemento getMemento(String version) { return store.get(version); } } public class DemoMain { public static void main(String[] args) { BillInfo billInfo = new BillInfo(); billInfo.setHead("start"); billInfo.setDetails(Lists.newArrayList()); BillStateProxy billState = new BillStateProxy(); // 快照版本记录 List<String> versions = Lists.newArrayList(); String currVersion = ""; // 单证初始编辑状态,编辑状态不允许发送 for (int i = 0; i < 15; i++) { billInfo.getDetails() .add(String.valueOf(i)); currVersion = billState.edit(billInfo, currVersion); versions.add(currVersion); } // 单证发送后变成已发送状态 billState.send(billInfo); // 已发送状态单证无法删除 billState.delete(billInfo); System.out.println("操作结束后结果 :" + billInfo); // 利用快照回滚单证内容 billState.restoreBillInfo(billInfo, versions.get(0)); System.out.println("回滚后结果 :" + billInfo); } }
输入如下:
-
突然想到 播放器的倍速播放似乎可以使用 State 模式?
3. 总结
扩展思路
- 分而治之:当遇到庞大且复杂的问题时,我们一般都会将问题分解为多个小问题逐个解决。而在 State 模式中,我们可以用类来表示状态,并且可以为每种具体的都定义一个相应的类,这样问题就被分解了。对于每种状态在对应的实例中实现对应的操作,而无需考虑其他因素,更为重要的是无需通过 if 判断多种情况,从而导致分支过多的情况。
- 依赖于状态的处理:在 State 接口中声明的所有方法都是“依赖于状态的处理”,都是“状态不同处理也不同”。
- 谁来管理状态迁移:用类来表示状态,将依赖于状态的处理分散在每个 ConcreteState 角色中,这里需要注意应该是谁来管理状态迁移,除了通过代码配置实现,还可以通过状态迁移表来实现,这种实现即根据 “输入和内部状态” 得到 “输出和下一个状态”。
- 不会自相矛盾:如果不使用 State 模式,我们需要用使用多个变量的值的集合来表示系统的状态,这时则需要注意不能让变量之间互相矛盾。而在 State 模式中使用类来表示状态,这样只需要一个表示系统状态的变量即可。
- 易于增加新的状态:在 State模式中增加新的状态很简单,编写一个实现 State 接口的实例即可,但是在 State 模式中增加其他“依赖状态的处理”则很困难,因为我们需要再 State 接口中增加新的方法,并且让所有子类实现它。
相关设计模式
- Singleton 模式: Singleton 模式常常会出现在 ConcreteState角色中。
- Flyweight 模式:在表示状态的类中并没有定义任何实例字段,因此有时我们可以使用 Flyweight 模式在多个 Context 角色之间共享 ConcreteState 角色。