写点什么

【愚公系列】2022 年 05 月 二十三种设计模式 (十八)- 备忘录模式 (Memento Pattern)

作者:愚公搬代码
  • 2022 年 5 月 29 日
  • 本文字数:4246 字

    阅读完需:约 14 分钟

前言

设计模式(Design pattern)是一套被反复使用、多数人知晓的、经过分类编目的、代码设计经验的总结。使用设计模式是为了可重用代码、让代码更容易被他人理解、保证代码可靠性。 毫无疑问,设计模式于己于他人于系统都是多赢的,设计模式使代码编制真正工程化,设计模式是软件工程的基石,如同大厦的一块块砖石一样。项目中合理的运用设计模式可以完美的解决很多问题,每种模式在现在中都有相应的原理来与之对应,每一个模式描述了一个在我们周围不断重复发生的问题,以及该问题的核心解决方案,这也是它能被广泛应用的原因。

一、备忘录模式(Memento Pattern)

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


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

二、使用步骤

角色

1、原发器(Originator)


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


2、备忘录(Memento)


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


3、备忘录管理者(Caretaker)


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

示例


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


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)!--------------------------->
复制代码


<hr style=" border:solid; width:100px; height:1px;" color=#000000 size=1">

总结

优点

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

缺点

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

使用场景

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

发布于: 2022 年 05 月 29 日阅读数: 14
用户头像

还未添加个人签名 2022.03.01 加入

该博客包括:.NET、Java、前端、IOS、Android、鸿蒙、Linux、物联网、网络安全、python、大数据等相关使用及进阶知识。查看博客过程中,如有任何问题,皆可随时沟通。

评论

发布
暂无评论
【愚公系列】2022年05月 二十三种设计模式(十八)-备忘录模式(Memento Pattern)_5月月更_愚公搬代码_InfoQ写作社区