C#设计模式之备忘录模式
总目录
前言
在开发过程中,我们经常会遇到需要“恢复”到之前状态的需求。无论是为了撤销用户的操作,还是恢复数据到某个安全的检查点,如何优雅地实现这些功能?这就引出了设计模式中的一个重要模式——备忘录模式(Memento Pattern)。
备忘录模式就是对某个类的状态进行保存下来,等到需要恢复的时候,可以从备忘录中进行恢复。生活中这样的例子也经常看到,如备份电话通讯录,备份操作操作系统,备份数据库等。
1 基础介绍
- 定义:在不破坏封装的前提下,捕获一个对象的内部状态,并在该对象之外保存这个状态,这样以后就可以把该对象恢复到原先的状态。
- 在软件构建过程中,某些对象的状态在转换的过程中,可能由于某种需要,要求程序能够回溯到对象之前处于某个点时的状态。如果使用一些公有接口来让其他对象得到对象的状态,便会暴露对象的细节实现。此时便需要使用到备忘录模式。
- 备忘录模式中的角色
- 发起人角色(Originator):记录当前时刻的内部状态,负责创建和恢复备忘录数据。负责创建一个备忘录Memento,用以记录当前时刻自身的内部状态,并可使用备忘录恢复内部状态。Originator【发起人】可以根据需要决定Memento【备忘录】存储自己的哪些内部状态。
- 备忘录角色(Memento):负责存储发起人对象的内部状态,在进行恢复时提供给发起人需要的状态,并可以防止Originator以外的其他对象访问备忘录。备忘录有两个接口:Caretaker【管理角色】只能看到备忘录的窄接口,他只能将备忘录传递给其他对象。Originator【发起人】却可看到备忘录的宽接口,允许它访问返回到先前状态所需要的所有数据。
- 管理者角色(Caretaker):负责保存备忘录对象Memento,不能对Memento的内容进行访问或者操作。
2 使用场景
应用场景:
- 如果系统需要提供回滚操作时,使用备忘录模式非常合适。例如文本编辑器的Ctrl+Z撤销操作的实现,数据库中事务操作。
- 保存一个对象在某一个时刻的状态或部分状态,这样以后需要时它能够恢复到先前的状态。
- 如果用一个接口来让其他对象得到这些状态,将会暴露对象的实现细节并破坏对象的封装性,一个对象不希望外界直接访问其内部状态,通过负责人可以间接访问其内部状态。
- 有时一些发起人对象的内部信息必须保存在发起人对象以外的地方,但是必须要由发起人对象自己读取,这时,使用备忘录模式可以把复杂的发起人内部信息对其他的对象屏蔽起来,从而可以恰当地保持封装的边界。
应用案例:
-
撤销功能
备忘录模式最常见的应用场景之一就是实现撤销功能。在编辑器、绘图软件等操作频繁的应用中,通过备忘录模式可以方便地将系统状态保存为多个历史记录,用户可以根据需求回到任意时刻的状态。 -
事务回滚
在数据库操作或文件系统中,事务的状态回滚也是备忘录模式的一个常见应用。假设一组操作失败或部分完成后,我们可以通过备忘录将系统状态回滚到初始状态,确保数据一致性。 -
游戏存档
在游戏开发中,备忘录模式可用于保存玩家的游戏进度,玩家可以选择在不同的存档点之间自由切换。每个存档点就是一个备忘录的实例。
3 实现方式
public class ContactPerson
{
//姓名
public string Name { get; set; }
//手机号
public string Phone { get; set; }
}
// 手机拥有者--相当于【发起人角色】Originator
public class MobileOwner
{
//通讯录(联系人列表)
//这是 发起人需要保存的内部状态(将来需要备份的数据)
private List<ContactPerson> _contactPersonList;
public List<ContactPerson> ContactPersonList
{
get
{
return this._contactPersonList;
}
set
{
this._contactPersonList = value;
}
}
//初始化需要备份的通讯录
public MobileOwner(List<ContactPerson> personList)
{
if (personList != null)
{
this._contactPersonList = personList;
}
else
{
throw new ArgumentNullException("参数不能为空!");
}
}
// 创建备忘录对象实例
// 将当前要保存的联系人列表保存到备忘录对象中
public ContactPersonMemento CreateMemento()
{
// 这里也应该传递深拷贝,new List方式传递的是浅拷贝,
// 因为ContactPerson类中都是string类型,所以这里new list方式对ContactPerson对象执行了深拷贝
// 如果ContactPerson包括非string的引用类型就会有问题,所以这里也应该用序列化传递深拷贝
return new ContactPersonMemento(new List<ContactPerson>(this._contactPersonList));
}
// 将备忘录中的数据备份还原到联系人列表中
public void RestoreMemento(ContactPersonMemento memento)
{
if (memento != null)
{
// 下面这种方式是错误的,因为这样传递的是引用,
// 则删除一次可以恢复,但恢复之后再删除的话就恢复不了.
// 所以应该传递contactPersonBack的深拷贝,深拷贝可以使用序列化来完成
this.ContactPersonList = memento.ContactPersonListBack;
}
}
public void Show()
{
Console.WriteLine($"联系人列表中共有{ContactPersonList.Count}个人,他们是:");
foreach (ContactPerson p in ContactPersonList)
{
Console.WriteLine($"姓名: {p.Name} 号码: {p.Phone}");
}
}
}
// 备忘录对象
// 用于保存状态数据,保存的是当时对象具体状态数据--相当于【备忘录角色】Memento
public class ContactPersonMemento
{
// 保存发起人创建的电话名单数据,就是所谓的状态
public List<ContactPerson> ContactPersonListBack { get; private set; }
public ContactPersonMemento(List<ContactPerson> personList)
{
ContactPersonListBack = personList;
}
}
// 管理角色
// 它可以管理【备忘录】对象
//如果是保存多个【备忘录】对象,当然可以对保存的对象进行增、删等管理处理
//相当于【管理者角色】Caretaker
public class MementoManager
{
//保存单个备忘对象 实现如下:
//public ContactPersonMemento ContactPersonMemento { get; set; }
//保存多个备忘对象,实现如下:
// 使用多个备忘录来存储多个备份点
public Dictionary<string, ContactPersonMemento> ContactPersonMementoDic { get; set; }
public MementoManager()
{
ContactPersonMementoDic = new Dictionary<string, ContactPersonMemento>();
}
}
class Program
{
static void Main(string[] args)
{
List<ContactPerson> persons = new List<ContactPerson>()
{
new ContactPerson() { Name="孙行者", Phone = "13500000000"},
new ContactPerson() { Name="者行孙", Phone = "13900000000"},
new ContactPerson() { Name="行者孙", Phone = "13100000000"}
};
MobileOwner mobileOwner = new MobileOwner(persons);
mobileOwner.Show();
// 创建备忘录并保存备忘录对象
MementoManager mementoManager = new MementoManager();
mementoManager.ContactPersonMementoDic.Add(DateTime.Now.ToString(), mobileOwner.CreateMemento());
// 更改发起人联系人列表
Console.WriteLine("----移除最后一个联系人--------");
mobileOwner.ContactPersonList.RemoveAt(2);
mobileOwner.Show();
// 创建第二个备份
Thread.Sleep(1000);
mementoManager.ContactPersonMementoDic.Add(DateTime.Now.ToString(), mobileOwner.CreateMemento());
// 恢复到原始状态
Console.WriteLine("-------恢复联系人列表,请从以下列表选择恢复的日期------");
var keyCollection = mementoManager.ContactPersonMementoDic.Keys;
foreach (string k in keyCollection)
{
Console.WriteLine($"Key = {k}");
}
while (true)
{
Console.Write("请输入数字,按窗口的关闭键退出:");
int index = -1;
try
{
index = Int32.Parse(Console.ReadLine());
}
catch
{
Console.WriteLine("输入的格式错误");
continue;
}
ContactPersonMemento contactPersonMemento = null;
if (index < keyCollection.Count && mementoManager.ContactPersonMementoDic.TryGetValue(keyCollection.ElementAt(index), out contactPersonMemento))
{
mobileOwner.RestoreMemento(contactPersonMemento);
mobileOwner.Show();
}
else
{
Console.WriteLine("输入的索引大于集合长度!");
}
}
}
}
4 优缺点分析
优点:
- 如果某个操作错误地破坏了数据的完整性,此时可以使用备忘录模式将数据恢复成原来正确的数据。
- 备份的状态数据保存在发起人角色之外,这样发起人就不需要对各个备份的状态进行管理。而是由备忘录角色进行管理,而备忘录角色又是由管理者角色管理,符合单一职责原则。
- 提供了一种状态恢复的实现机制,使得用户可以方便地回到一个特定的历史步骤,当新的状态无效或者存在问题时,可以使用先前存储起来的备忘录将状态复原。
缺点:
- 在实际的系统中,可能需要维护多个备份,需要额外的资源,这样对资源的消耗比较严重。资源消耗过大,如果类的成员变量太多,就不可避免占用大量的内存,而且每保存一次对象的状态都需要消耗内存资源,如果知道这一点大家就容易理解为什么一些提供了撤销功能的软件在运行时所需的内存和硬盘空间比较大了。
结语
希望以上内容可以帮助到大家,如文中有不对之处,还请批评指正。
参考资料:
【C#设计模式-备忘录模式】
深入探索 C# 中的备忘录模式:实现数据的回滚与恢复
使用 C# 实现23种常见的设计模式
C#设计模式(23)——备忘录模式(Memento Pattern)
C#设计模式之二十二备忘录模式(Memento Pattern)【行为型】