一、场景问题
某手游公司经营一款中国象棋游戏,在用户的反馈中发现一些用户由于屏幕操作失误经常出现下错棋的情况,但是系统并未提供撤销的功能,现需要设计一套撤销的功能
- 可以回退棋子状态
- 可以取消回退
请设计一条切实可用的程序。
二、传统解决方案
一种解决思路就是每个棋子对象存储自己走过的快照信息,根据不同的时间恢复到指定的状态。这种方式简单可行,但是过多的信息混合到实体对象中,导致对象臃肿,违背单一职责原则。针对这种情况,我们可以进行指责分离,抽出一个单独的备忘录来优化。这就引出了本文的主题——备忘录模式。
三、模式剖析
1、模式定义
备忘录模式(Memento Pattern):在不破坏封装性的前提下,捕获一个对象的内部状态,并在该对象之外保存这个状态,以便以后当需要时能将该对象恢复到原先保存的状态。该模式又叫快照模式。
2、模式结构
备忘录模式一般包含如下角色
-
Originator
:原发器记录当前时刻的内部状态信息,提供创建备忘录和恢复备忘录数据的功能,实现其他业务功能,它可以访问备忘录里的所有信息。
//发起人 class Originator { private String state; public void setState(String state) { this.state=state; } public String getState() { return state; } public Memento createMemento() { return new Memento(state); } public void restoreMemento(Memento m) { this.setState(m.getState()); } }
-
Memento
:备忘录负责存储发起人的内部状态,在需要的时候提供这些内部状态给发起人。
//备忘录 class Memento { private String state; public Memento(String state) { this.state=state; } public void setState(String state) { this.state=state; } public String getState() { return state; } }
-
Caretaker
:管理者对备忘录进行管理,提供保存与获取备忘录的功能,但其不能对备忘录的内容进行访问与修改
//管理者 class Caretaker { private Memento memento; public void setMemento(Memento m) { memento=m; } public Memento getMemento() { return memento; } }
3、模式结构图
4、使用备忘录模式改进案例
4.1、代码展示
-
棋子类(原发器)
public class Chess { /** * 象棋类型 */ private String type; /** * 横坐标 */ private int x; /** * 纵坐标 */ private int y; public Chess(String type, int x, int y) { this.type = type; this.x = x; this.y = y; } /** * 移动位置 * @param x 目标地横坐标 * @param y 目标地纵坐标 * @return void **/ public void move(int x, int y) { this.x = x; this.y = y; display(); } /** * 保存当前状态 * @return **/ public ChessMemento save() { return new ChessMemento(type, x, y); } /** * 恢复状态 * @param chessMemento * @return void **/ public void restore(ChessMemento chessMemento) { if (chessMemento != null) { this.type = chessMemento.getType(); this.x = chessMemento.getX(); this.y = chessMemento.getY(); } display(); } //输出当前棋子信息 private void display() { System.out.println("当前" + type + "棋子位置:(" + x + "," + y + ")"); } }
-
象棋备忘录
public class ChessMemento { /** * 象棋类型 */ private String type; /** * 横坐标 */ private int x; /** * 纵坐标 */ private int y; public ChessMemento(String type, int x, int y) { this.type = type; this.x = x; this.y = y; } public String getType() { return type; } public void setType(String type) { this.type = type; } public int getX() { return x; } public void setX(int x) { this.x = x; } public int getY() { return y; } public void setY(int y) { this.y = y; } }
信息基本与象棋信息一致(实际情况下象棋的基本信息会非常多,例如样式、颜色等,备忘录信息只需要保存基本信息,例如坐标,类型等)
-
备忘录管理器
public class MementoManager { /** * 存放一系列的备忘录信息 */ private List<ChessMemento> mementos = new ArrayList<>(); /** * 游标 */ private int index = -1; /** * 获取当前的回退状态 * * @return com.jicl.design.memento.ChessMemento * @author xianzilei * @date 2020/11/12 17:55 **/ public ChessMemento getLastMemento() { if (index <= 0) { return null; } return mementos.get(--index); } /** * 获取当前的下一状态 * * @return com.jicl.design.memento.ChessMemento * @author xianzilei * @date 2020/11/12 18:01 **/ public ChessMemento getNextMemento() { if (index >= mementos.size() - 1) { return null; } return mementos.get(++index); } /** * 保存状态 * * @param memento 1 * @return void * @author xianzilei * @date 2020/11/12 18:47 **/ public void saveMemento(ChessMemento memento) { mementos.add(memento); index++; } }
这里采用list集合存储象棋状态,游标标记当前状态的位置。
4.2、客户端测试
public static void main(String[] args) {
//创建象棋和象棋管理器
MementoManager mementoManager = new MementoManager();
Chess chess = new Chess("车", 0, 0);
//存档
mementoManager.saveMemento(chess.save());
//移动棋子并存档
chess.move(0, 5);
mementoManager.saveMemento(chess.save());
//移动棋子并存档
chess.move(4, 5);
mementoManager.saveMemento(chess.save());
//移动棋子并存档
chess.move(8, 5);
mementoManager.saveMemento(chess.save());
//移动棋子并存档
chess.move(8, 8);
mementoManager.saveMemento(chess.save());
//回退
System.out.println("回退一步,");
chess.restore(mementoManager.getLastMemento());
//回退
System.out.println("回退一步,");
chess.restore(mementoManager.getLastMemento());
//回退
System.out.println("回退一步,");
chess.restore(mementoManager.getLastMemento());
//回退
System.out.println("回退一步,");
chess.restore(mementoManager.getLastMemento());
//回退
System.out.println("回退一步,");
chess.restore(mementoManager.getLastMemento());
//撤销回退
System.out.println("撤销回退一步,");
chess.restore(mementoManager.getNextMemento());
}
输出
当前车棋子位置:(0,5)
当前车棋子位置:(4,5)
当前车棋子位置:(8,5)
当前车棋子位置:(8,8)
回退一步,
当前车棋子位置:(8,5)
回退一步,
当前车棋子位置:(4,5)
回退一步,
当前车棋子位置:(0,5)
回退一步,
当前车棋子位置:(0,0)
回退一步,
当前车棋子位置:(0,0)
撤销回退一步,
当前车棋子位置:(0,5)
当前车棋子位置:(8,0)
回退一步,
当前车棋子位置:(0,5)
5、优缺点
- 优点
- 它提供了一种状态恢复的实现机制,使得用户可以方便地回到一个特定的历史步骤,当新的状态无效或者存在问题时,可以使用暂时存储起来的备忘录将状态复原。
- 备忘录实现了对信息的封装,一个备忘录对象是一种原发器对象状态的表示,不会被其他代码所改动。备忘录保存了原发器的状态,采用列表、堆栈等集合来存储备忘录对象可以实现多次撤销操作
- 缺点
- 资源消耗过大,如果需要保存的原发器类的成员变量太多,就不可避免需要占用大量的存储空间,每保存一次对象的状态都需要消耗一定的系统资源
6、使用场景
- 保存一个对象在某一个时刻的全部状态或部分状态,这样以后需要时它能够恢复到先前的状态,实现撤销操作。
- 防止外界对象破坏一个对象历史状态的封装性,避免将对象历史状态的实现细节暴露给外界对象。