19、C#设计模式 - 备忘录模式

备忘录模式(Memento Pattern)

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

备忘录模式为我们提供了“后悔药”的机制,为我们在需要的时候,可以将对对象的修改撤销甚至重做。

角色:

1、 原发器(Originator);

创建一个备忘录,用以记录当前时刻它的内部状态,在需要时使用备忘录恢复内部状态;

2、 备忘录(Memento);

将原发器对象的内部状态存储起来;

3、 备忘录管理者(Caretaker);

负责保存好备忘录,不能对备忘录的内容进行操作或检查。

示例:

 

命名空间MementoPattern中包含Memento备忘录类,Caretaker管理者类,象棋Chessman类。本案例将向大家演示如何撤销或重做对象棋位置的修改操作。本案例使用栈以支持多次撤销,并且重做支持前1次的多次撤销。本案例不支持重新设置棋子位置时所产生的分支。

namespace MementoPattern
public partial class Chessman {

	private Point _position;

	private Caretaker _caretaker = null;

	public Point Position {
		get => _position;
		set {
			_position = value;
			_caretaker.Memento.Position = value; Console.WriteLine(
			 String.Format(Const.POSITION_INFO, _position.X, _position.Y));
		}
	}

	public Chessman() : this(new Point(0, 0)) {

	}

	public Chessman(Point point) {
		_caretaker = new Caretaker(new Memento());
		Position = point;
	}

}

象棋棋子类Chessman,内部维持棋子的位置,在设置棋子位置时将信息保存到管理者所管理的备忘录中。

public partial class Chessman {

	public Chessman Undo(int step) {
		try {
			Console.WriteLine(Const.ARROW_LEFT);
			Console.WriteLine($"Undo({step})!");
			this._position = _caretaker.Memento.Undo(step);
			Console.WriteLine(
				String.Format(Const.POSITION_INFO, _position.X, _position.Y));
			Console.WriteLine(Const.ARROW_RIGHT);
			return this;
		} catch(Exception ex) {
			Console.WriteLine(ex.Message);
			Console.WriteLine(Const.ARROW_RIGHT);
			return this;
		}
	}

	public Chessman Redo() {
		try {
			Console.WriteLine(Const.ARROW_LEFT);
			Console.WriteLine("Redo()!");
			this._position = _caretaker.Memento.Redo();
			Console.WriteLine(
				String.Format(Const.POSITION_INFO, _position.X, _position.Y));
			Console.WriteLine(Const.ARROW_RIGHT);
			return this;

		} catch(Exception ex) {
			Console.WriteLine(ex.Message);
			Console.WriteLine(Const.ARROW_RIGHT);
			return this;
		}
	}

}

象棋棋子类Chessman的第2部分(partial),支持按步数撤销位置,并且支持重做命令。

public partial class Memento {

	private Point _position;

	public Point Position {
		get => _position;
		set {
			_position = value;
			_history.Push(new RedoInfo { Position = value });
			_redoList.Clear();
		}
	}

	public Memento() {
		_history = new Stack<RedoInfo>();
		_redoList = new Stack<RedoInfo>();
	}

	private Stack<RedoInfo> _history = null;
	private Stack<RedoInfo> _redoList = null;

	public Point Undo(int step) {
		int totalCount = 0;
		List<string> temp = new List<string>();
		foreach(var item in _history) {
			if(string.IsNullOrWhiteSpace(item.GUID)) {
				totalCount++;
			} else {
				if(!temp.Contains(item.GUID)) {
					totalCount++;
					temp.Add(item.GUID);
				}
			}
		}
		if(step >= totalCount) {
			throw new InvalidOperationException("Too much steps!");
		}
		var guid = Guid.NewGuid().ToString("B");
		for(int i = 1; i <= step; i++) {
			Undo(guid);
		}
		return _position;
	}

}

备忘录类Memento,内部维持对位置的引用并用2个栈分别管理历史和重做数据。

public partial class Memento {

	private void UndoLoop(string guid) {
		var history = _history.Pop();
		history.GUID = guid;
		_redoList.Push(history);
		_position = _history.Peek().Position;
	}

	private void Undo(string guid) {
		var temp = _history.Peek().GUID;
		if(string.IsNullOrWhiteSpace(temp)) {
			UndoLoop(guid);
		} else {
			while(_history.Peek().GUID == temp) {
				UndoLoop(guid);
			}
		}
	}

	public Point Redo() {
		if(_redoList.Count == 0) {
			throw new InvalidOperationException("You can not redo now!");
		}
		var guid = _redoList.Peek().GUID;
		while(_redoList.Count != 0 && _redoList.Peek().GUID == guid) {
			_history.Push(_redoList.Pop());
			_position = _history.Peek().Position;
		}
		return _position;
	}

}

备忘录类Memento的第2部分(partial),包含了按步数撤销和重做的具体逻辑。

public class Caretaker {

    public Memento Memento { get; set; }

    public Caretaker(Memento memento) {
        Memento = memento;
    }

}

管理者Caretaker类,管理者负责维持备忘录。

public class RedoInfo {

    public Point Position { get; set; }

    public string GUID { get; set; }

}

重做RedoInfo类,按GUID的值判断是否是同一“批次”被撤销的。

public class Const {

    public const string POSITION_INFO = "Current position is ({0},{1})!";

    public const string ARROW_LEFT = "<---------------------------";

    public const string ARROW_RIGHT = "--------------------------->";

}

常量类Const,维护一些本案例中经常用到的字符串。在实际开发过程中不应当有此类,应该将相应的常量放在具体要使用的类中。2017年,阿里发布《阿里巴巴Java开发手册》,其中有一节提到此准则,所有使用面向对象编程语言的开发人员都应当遵从。

public class Program {

    private static Chessman _chessman = null;

    public static void Main(string[] args) {
        _chessman = new Chessman(new Point(1, 10));
        _chessman.Position = new Point(2, 20);
        _chessman.Position = new Point(3, 30);
        _chessman.Position = new Point(4, 40);
        _chessman.Position = new Point(5, 50);
        _chessman.Position = new Point(9, 40);

        _chessman.Undo(1)
                 .Undo(2)
                 .Undo(1)
                 .Redo()
                 .Redo()
                 .Redo()
                 .Redo()
                 .Redo()
                 .Undo(6)
                 .Undo(5)
                 .Undo(4);

        Console.ReadKey();
    }

}

以上是调用方的代码演示,撤销和重做方法经过特殊处理以支持方法链。以下是这个案例的输出结果:

Current position is (1,10)!
Current position is (2,20)!
Current position is (3,30)!
Current position is (4,40)!
Current position is (5,50)!
Current position is (9,40)!
<---------------------------
Undo(1)!
Current position is (5,50)!
--------------------------->
<---------------------------
Undo(2)!
Current position is (3,30)!
--------------------------->
<---------------------------
Undo(1)!
Current position is (2,20)!
--------------------------->
<---------------------------
Redo()!
Current position is (3,30)!
--------------------------->
<---------------------------
Redo()!
Current position is (5,50)!
--------------------------->
<---------------------------
Redo()!
Current position is (9,40)!
--------------------------->
<---------------------------
Redo()!
You can not redo now!
--------------------------->
<---------------------------
Redo()!
You can not redo now!
--------------------------->
<---------------------------
Undo(6)!
Too much steps!
--------------------------->
<---------------------------
Undo(5)!
Too much steps!
--------------------------->
<---------------------------
Undo(4)!
Current position is (1,10)!
--------------------------->

优点:

1、 给用户提供了一种可以恢复状态的机制,可以使用户能够比较方便地回到某个历史的状态;
2、 实现了信息的封装,使得用户不需要关心状态的保存细节;

缺点:

1、 如果类的成员变量过多,势必会占用比较大的资源,而且每一次保存都会消耗一定的内存;

使用场景:

1、 需要保存/恢复数据的相关状态场景;
2、 提供一个可回滚的操作;