Java设计模式-备忘录模式

备忘录(Memento)模式,有时也被称为快照(Snapshot)模式或者Token模式,在23种设计模式中属于对象行为型模式。

备忘录模式:在不破坏封装的前提下,捕获一个对象的内部状态,并在该对象之外保存这个状态,这样可以在以后将对象恢复到原先保存的状态。

模式介绍

备忘录模式,直接根据模式名称理解它的使用场景可能让人有些困惑,其实备忘录模式提供的就是软件实现中的一种历史记录机制,或者说是“悔棋”机制。如果玩过象棋、五子棋或者围棋等一些围棋类小游戏,在这些游戏中一般会设置一个悔棋按钮,目的就是为了防止某些“菜鸟”不小心走错了棋,或者因为不熟悉电脑或者手机操作,误操作了游戏。一旦用户走错棋或者操作失误可以恢复到上一个步骤。

适用性

  • 需要保存一个对象的全部或者部分状态,这样在以后需要的时候可以恢复的先前的状态,实现撤销操作。
  • 如果一个用接口来让其它对象直接得到这些状态,将会暴露对象的实现细节并破坏对象的封装性。

示例一

这里就以中国象棋为示例介绍。

// 棋子类 原发器
public class Chessman {
	private String label;
	private int x;
	private int y;

	public Chessman(String label, int x, int y) {
		this.label = label;
		this.x = x;
		this.y = y;
	}

	// 省略了getter和setter方法

	// 保存状态
	public ChessmanMemento save() {
		return new ChessmanMemento(this.label, this.x, this.y);
	}

	// 恢复状态
	public void restore(ChessmanMemento chessmanMemento) {
		this.label = chessmanMemento.getLabel();
		this.x = chessmanMemento.getX();
		this.y = chessmanMemento.getY();
	}
}
// 棋子备忘录类
public class ChessmanMemento {
	private String label;
	private int x;
	private int y;
	
	public ChessmanMemento(String label, int x, int y) {
		this.label = label;
		this.x = x;
		this.y = y;
	}
	
	// 省略了getter和setter方法
}
public class MementoCaretaker {
	private ChessmanMemento chessmanMemento;

	public ChessmanMemento getMemento() {
		return chessmanMemento;
	}

	public void setMemento(ChessmanMemento chessmanMemento) {
		this.chessmanMemento = chessmanMemento;
	}
}
public class Client {
	public static void main(String[] args) {
		MementoCaretaker mc = new MementoCaretaker();
		Chessman chess = new Chessman("車", 1, 1);
		display(chess);

		mc.setMemento(chess.save());
		chess.setY(3);
		display(chess);

		mc.setMemento(chess.save());
		chess.setY(5);
		display(chess);

		System.out.println("******* 悔棋 *******");
		chess.restore(mc.getMemento());
		display(chess);
	}
	public static void display(Chessman chess) {
		System.out.println(String.format("棋子[%s]:当前位置为:[%d, %d]", chess.getLabel(), chess.getX(), chess.getY()));
	}
}

示例输出结果如下:

棋子[車]:当前位置为:[1, 1]
棋子[車]:当前位置为:[1, 3]
棋子[車]:当前位置为:[1, 5]
******* 悔棋 *******
棋子[車]:当前位置为:[1, 3]

示例二

上面的示例“悔棋”只能执行一次,接下来我们看一下如何设计一个可以多次“悔棋”的代码场景。

既然设计多次“悔棋”,那么在负责人类中存储的备忘录应该是一个集合或者数组形式的,其余的代码逻辑不变。

public class MementoCaretaker {
	private List memetoList=new ArrayList<>();;

	public ChessmanMemento getMemento(int i) {
		return memetoList.get(i);
	}

	public void setMemento(ChessmanMemento chessmanMemento) {
		memetoList.add(chessmanMemento);
	}
}
public class Client {
	private static int index = -1;
	private static MementoCaretaker mc = new MementoCaretaker();

	public static void main(String[] args) {
		Chessman chess = new Chessman("車", 1, 1);
		play(chess);
		chess.setY(3);
		play(chess);
		chess.setY(5);
		play(chess);

		undo(chess, index);
		undo(chess, index);
		
		redo(chess, index);
		redo(chess, index);
	}

	public static void play(Chessman chess) {
		mc.setMemento(chess.save());
		index++;
		display(chess);
	}

	public static void undo(Chessman chess, int i) {
		System.out.println("\n******* 悔棋 *******");
		index--;
		chess.restore(mc.getMemento(i - 1));
		display(chess);
	}

	public static void redo(Chessman chess, int i) {
		System.out.println("\n******* 撤销悔棋 *******");
		index++;
		chess.restore(mc.getMemento(i + 1));
		display(chess);
	}

	private static void display(Chessman chess) {
		System.out.println(String.format("棋子[%s]:当前位置为:[%d, %d]", chess.getLabel(), chess.getX(), chess.getY()));
	}
}

示例输出结果如下:

棋子[車]:当前位置为:[1, 1]
棋子[車]:当前位置为:[1, 3]
棋子[車]:当前位置为:[1, 5]

******* 悔棋 *******
棋子[車]:当前位置为:[1, 3]

******* 悔棋 *******
棋子[車]:当前位置为:[1, 1]

******* 撤销悔棋 *******
棋子[車]:当前位置为:[1, 3]

******* 撤销悔棋 *******
棋子[車]:当前位置为:[1, 5]

示例三

备忘录模式的一般性示例如下:

// 原发器
public class Originator {
	private String state;
	
	// 省略了getter和setter方法

	// 创建备忘录
	public Memento createMemento() {
		return new Memento(this);
	}

	// 从备忘录恢复状态
	public void restoreMemento(Memento memento) {
		this.state = memento.getState();
	}
}
// 备忘录
public class Memento {
	private String state;

	public Memento(Originator originator) {
		this.state = originator.getState();
	}

	// 省略了getter和setter方法
}

// 负责人
public class MementoCaretaker {
	private Memento memento;

	public Memento getMemento() {
		return memento;
	}

	public void setMemento(Memento chessmanMemento) {
		this.memento = chessmanMemento;
	}
}
public class Client {
	public static void main(String[] args) {
		MementoCaretaker mc = new MementoCaretaker();
		Originator originator = new Originator();
		
		originator.setState("state:1");
		display(originator);
		
		// 在执行之前先创建备忘录,并放在负责人中保存
		mc.setMemento(originator.createMemento());
		originator.setState("state:3");
		display(originator);
		
		mc.setMemento(originator.createMemento());
		originator.setState("state:4");
		display(originator);
		
		System.out.println("*********撤销********");
		originator.restoreMemento(mc.getMemento());
		display(originator);
	}

	public static void display(Originator originator) {
		System.out.println("当前状态 "+originator.getState());
	}
}

示例输出结果如下:

当前状态 state:1
当前状态 state:3
当前状态 state:4
*********撤销********
当前状态 state:3

模式分析

备忘录模式类图如下:

  • 原发器Originator:用于创建备忘录,并且可以记录当前时刻它的状态;使用备忘录恢复其内部状态。
  • 备忘录Memento:存储原发器的内部状态,可以根据原发器决定保存哪些状态。备忘录的设计一般可以参考原发器的设计,根据实际需要确定保存哪些原发器的哪些属性。需要注意,备忘录除了原发器或者负责人之外,备忘录对象不能被其它类直接调用。
  • 负责人Caretaker:负责人又称为管理者,负责保存备忘录,但又不能对备忘录的内存进行操作或者检查。在负责人类中可以保存一个或者多个备忘录对象,它只负责保存备忘录对象,不能修改对象,也无需知道对象的实现细节。

在设计备忘录类可以将原发器作为构造方法入参传入,也可以只把自己需要记录的属性作为入参传入保存。

创建备忘录,保存原发器当前执行的状态应该是在下一次执行操作之前,然后放入负责人中保存,这样备忘录中存储的才是上一次原发器执行后的状态。

模式优点

提供了一种状态恢复机制,使得用户可以方便地回到某一个特定的历史步骤,当新的状态无效或者存在问题时,可以使用先前存储起来的备忘录将状态复原。

实现了内部状态的封装。除了创建它的用户对象之外,其它对象都不能够访问这些状态信息。

模式缺点

资源消耗过大,如果需要保存的历史记录过多,不可避免的会产生大量的备忘录对象,而且这些对象一般都是保存在内存中的。

与其它模式关系

在命令模式中支持可撤销的操作,那么可以使用备忘录模式保存可撤销操作的状态。

原发器在创建备忘录时,可以借助原型模式创建备忘录。

在保存多态历史记录时,遍历备忘录可以借助于迭代器模式。

小结

虽然撤销或者历史记录操作在很多软件的实现中很常见,但是在普通应用软件开发中备忘录模式使用频率并不是很高。

理解备忘录模式很容易,关键在于如何设计备忘录类和负责人类。备忘录类保存了原发器操作过程中的中间状态,因此需要注意,不可以让其它类访问到备忘录类,特别是不允许其它类修改备忘录类。

设计模式就是前人总结的,用于解决在特定条件下重复出现的软件设计问题而提供的一种合理的、有效的解决方案,它融合了许多专家的设计经验。学习设计模式一定要注意,每一种设计模式都有不同的角色,明确各角色的责任是学习掌握该设计模式的关键所在,这也是所谓的专人专事。

示例源代码下载,提取码:fmxo

评论

您确定要删除吗?删除之后不可恢复