如果再回到从前——备忘录模式

如果再回到从前——备忘录模式

继上一篇“在NBA我需要翻译——适配器模式”后,本文继续讲解《大话设计模式》第18章“如果再回到从前——备忘录模式”。喜欢本书请到各大商城购买原书,支持正版。

大话设计模式前期读书系列:

本文正式开始


1 如果再给我一次机会…

时间:5月6日18点 地点:小菜大鸟住所的客厅 人物:小菜、大鸟

“小菜,今天上午看NBA了吗?火箭季后赛第七场对爵士的比赛。”大鸟问道。

“没有,不过结果倒是在网上第一时间就知道了。在最后比赛剩下4分钟时,比分还相同,到剩下57.8秒的时候,火箭也只落后2分,可惜,最后的两个进攻篮板球没得到,火箭就输掉了比赛。”

“是呀,最后一分钟的失误,几乎就等于输掉了整个赛季。”

“如果火箭任何一人能抓到两个篮板中的一个,结果可能完全不是这样。真是遗憾呀。”小菜感慨道。

“很多时候我们做了件事后,却开始后悔。这就是人类的内心软弱一面。时间不能倒流,不管怎么样人生是无法回到从前的,但是软件就不一样了。还记得玩一些单机的PC游戏的时候吗,通常我都是在打大Boss之前,先保存一个进度,然后如果通关失败了,我可以再返回刚才那个进度来恢复原来的状态,从头来过。从这点上说,我们比姚明强。”

“哈,这其中原理是不是就是把当前的游戏状态的各种参数存储,以便恢复时读取呢?”

“是的,通常这种保存都是存在磁盘上了,以便日后读取。但对于一些更为常规的应用,比如我们下棋时需要悔棋、编写文档时需要撤销、查看网页时需要后退,这些相对频繁而简单的恢复并不需要存在磁盘中,只要将保存在内存中的状态恢复一下即可。”

“嗯,这是更普通的应用,很多开发中都会用到。”

“那我简单说个场景,你想想看怎么用代码实现。游戏的某个场景,一游戏角色有生命力、攻击力、防御力等等数据,在打Boss 前和后一定会不一样的,我们允许玩家如果感觉与Boss 决斗的效果不理想可以让游戏恢复到决斗前。”

“好的,我试试看。”

2 游戏存进度

游戏角色类,用来存储角色的生命力、攻击力、防御力的数据。

/// <summary>
/// 游戏角色类,用来存储角色的生命力、攻击力、防御力的数据
/// </summary>
class GameRole
{
  /// <summary>
  /// 生命力
  /// </summary>
  public int Vitality { get; set; }

  /// <summary>
  /// 攻击力
  /// </summary>
  public int Attack { get; set; }

  /// <summary>
  /// 防御力
  /// </summary>
  public int Defense { get; set; }

  /// <summary>
  /// 状态显示
  /// </summary>
  public void StateDisplay()
  {
    System.Console.WriteLine($"角色当前状态:\r\n体力:{Vitality}\r\n攻击力:{Attack}\r\n防御力:{Defense}");
  }

  /// <summary>
  /// 获得初始状态
  /// </summary>
  public void GetInitState()
  {
    // 数据通常来自本机磁盘或远程数据库
    this.Vitality = 100;
    this.Attack = 100;
    this.Defense = 100;
  }

  /// <summary>
  /// 战斗
  /// </summary>
  public void Fight()
  {
    // 在与Boss大战后游戏数据损耗为零
    this.Vitality = 0;
    this.Attack = 0;
    this.Defense = 0;
  }
}

客户端调用时

static void Main(string[] args)
{
  // 大战Boss前
  GameRole lixiaoyao = new GameRole();

  // 大战Boss前,获得初始角色状态
  lixiaoyao.GetInitState();
  lixiaoyao.StateDisplay();

  //保存进度
  GameRole backup = new GameRole();

  // 通过‘游戏角色’的新实例,来保存进度
  backup.Vitality = lixiaoyao.Vitality;
  backup.Attack = lixiaoyao.Attack;
  backup.Defense = lixiaoyao.Defense;

  //大战Boss时,损耗严重
  lixiaoyao.Fight();

  // 大战Boss 时, 损耗严重所有数据全部损耗为零
  lixiaoyao.StateDisplay();

  //恢复之前状态,GameOver不甘心,恢复之前进度,重新来玩
  lixiaoyao.Vitality = backup.Vitality;
  lixiaoyao.Attack = backup.Attack;
  lixiaoyao.Defense = backup.Defense;
  lixiaoyao.StateDisplay();

  Console.Read();
}

结果显示:

角色当前状态:
体力:100
攻击力:100
防御力:100
角色当前状态:
体力:0
攻击力:0
防御力:0
角色当前状态:
体力:100
攻击力:100
防御力:100

“小菜,这样的写法,确实是实现了我的要求,但是问题也确实多多。”

“哈,你的经典理论,代码无错未必优。说吧,我有心理准备。”

“问题主要在于这客户端的调用。下面这一段有问题,因为这样写就把整个游戏角色的细节暴露给了客户端,你的客户端的职责就太大了,需要知道游戏角色的生命力、攻击力、防御力这些细节,还要对它进行‘备份’。以后需要增加新的数据,例如增加‘魔法力’或修改现有的某种力,例如‘生命力’改为‘经验值’,这部分就一定要修改了。同样的道理也存在于恢复时的代码。”

如果再回到从前——备忘录模式

“显然,我们希望的是把这些‘游戏角色’的存取状态细节封装起来,而且最好是封装在外部的类当中。以体现职责分离。”

3 备忘录模式

“所以我们需要学习一个新的设计模式,备忘录模式。”

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

如果再回到从前——备忘录模式
备忘录模式(Memento)结构图

Originator(发起人):负责创建一个备忘录Memento,用以记录当前时刻它的内部状态,并可使用备忘录恢复内部状态。Originator可根据需要决定Memento存储Originator的哪些内部状态。

Memento(备忘录):负责存储Originator对象的内部状态,并可防止Originator 以外的其他对象访问备忘录Memento。备忘录有两个接口,Caretaker 只能看到备忘录的窄接口,它只能将备忘录传递给其他对象。Originator 能够看到一个宽接口,允许它访问返回到先前状态所需的所有数据。

Caretaker(管理者):负责保存好备忘录Memento,不能对备忘录的内容进行操作或检查。

“就刚才的例子,‘游戏角色’类其实就是一个Originator,而你用了同样的‘游戏角色’实例‘备份’来做备忘录,这在当需要保存全部信息时,是可以考虑的,而用clone的方式来实现Memento 的状态保存可能是更好的办法,但是如果是这样的话,使得我们相当于对上层应用开放了Originator的全部( public)接口,这对于保存备份有时候是不合适的。”

“那如果我们不需要保存全部的信息以备使用时,怎么办?”

“哈,对的,这或许是更多可能发生的情况,我们需要保存的并不是全部信息,而只是部分,那么就应该有一个独立的备忘录类Memento,它只拥有需要保存的信息的属性。”

4 备忘录模式基本代码

发起人(Originator)类

/// <summary>
/// 发起人类
/// </summary>
class Originator
{
  /// <summary>
  /// 需要保存的属性,可能有多个
  /// </summary>
  public string State { get; set; }

  /// <summary>
  /// 创建备忘录,将当前需要保存的信息导入实例化出一个Memento对象
  /// </summary>
  /// <returns></returns>
  public Memento CreateMemento()
  {
    return (new Memento(State));
  }

  /// <summary>
  /// 恢复备忘录,将Memento导入并将相关数据恢复
  /// </summary>
  /// <param name="memento"></param>
  public void SetMemento(Memento memento)
  {
    State = memento.State;
  }

  public void Show()
  {
    Console.WriteLine($"State={State}");
  }
}

备忘录(Memento)类

/// <summary>
/// 备忘录类
/// </summary>
class Memento
{
  public string State { get; private set; }

  /// <summary>
  /// 构造方法,将相关数据导入
  /// </summary>
  /// <param name="state"></param>
  public Memento(string state)
  {
    this.State = state;
  }
}

管理者(Caretaker)类

/// <summary>
/// 管理者类
/// </summary>
class Caretaker
{
  /// <summary>
  /// 得到或设置备忘录
  /// </summary>
  public Memento Memento { get; set; }
}

客户端程序

static void Main(string[] args)
{
  // Originator初始状态,状态属性为“On”
  Originator o = new Originator();
  o.State = "On ";
  o.Show();


  Caretaker c = new Caretaker();

  // 保存状态时,由于有了很好的封装,可以隐藏Originator的实现细节
  c.Memento = o.CreateMemento();

  // Originator改变了状态属性为“Off”
  o.State = "off";
  o.Show();

  // 恢复原初始状态
  o.SetMemento(c.Memento);
  o.Show();

  Console.Read();
}

“哈,我明白了,这当中就是把要保存的细节给封装在了Memento中了,哪一天要更改保存的细节也不用影响客户端了。那么这个备忘录模式都用在一些什么场合呢?”

“Memento模式比较适用于功能比较复杂的,但需要维护或记录属性历史的类,或者需要保存的属性只是众多属性中的一小部分时,Originator可以根据保存的Memento信息还原到前一状态。”


“我记得好像命令模式也有实现类似撤销的作用?”

“哈,小子记性不错,如果在某个系统中使用命令模式时,需要实现命令的撤销功能,那么命令模式可以使用备忘录模式来存储可撤销操作的状态[DP]。有时一些对象的内部信息必须保存在对象以外的地方,但是必须要由对象自己读取,这时,使用备忘录可以把复杂的对象内部信息对其他的对象屏蔽起来[DP],从而可以恰当地保持封装的边界。”

“我感觉可能最大的作用还是在**当角色的状态改变的时候,有可能这个状态无效,这时候就可以使用暂时存储起来的备忘录将状态复原[DP]**这个作用吧?”

“说得好,这当然是最重要的作用了。”

“明白,我学会了。”

“别急,你还没有把你刚才的代码改成备忘录模式的。”

“啊,你就不打算饶过我。等着,看我来拿满分。”

5 游戏进度备忘

如果再回到从前——备忘录模式
代码结构图

游戏角色类

(重点在新增的“保存角色状态”方法”SaveState”和“恢复角色状态”方法“RecoveryState”)

/// <summary>
/// 游戏角色类,用来存储角色的生命力、攻击力、防御力的数据
/// </summary>
class GameRole
{
  /// <summary>
  /// 生命力
  /// </summary>
  public int Vitality { get; set; }

  /// <summary>
  /// 攻击力
  /// </summary>
  public int Attack { get; set; }

  /// <summary>
  /// 防御力
  /// </summary>
  public int Defense { get; set; }

  /// <summary>
  /// 状态显示
  /// </summary>
  public void StateDisplay()
  {
    System.Console.WriteLine($"角色当前状态:\r\n体力:{Vitality}\r\n攻击力:{Attack}\r\n防御力:{Defense}");
  }

  /// <summary>
  /// 获得初始状态
  /// </summary>
  public void GetInitState()
  {
    // 数据通常来自本机磁盘或远程数据库
    this.Vitality = 100;
    this.Attack = 100;
    this.Defense = 100;
  }

  /// <summary>
  /// 战斗
  /// </summary>
  public void Fight()
  {
    // 在与Boss大战后游戏数据损耗为零
    this.Vitality = 0;
    this.Attack = 0;
    this.Defense = 0;
  }

  /// <summary>
  /// 保存角色状态,新增“保存角色状态”方法,将游戏角色的三个状态值通过实例化“角色状态存储箱”返回
  /// </summary>
  /// <returns></returns>
  public RoleStateMemento SaveState()
  {
    return (new RoleStateMemento(Vitality, Attack, Defense));
  }

  /// <summary>
  /// 恢复角色状态,新增“恢复角色状态”方法,可将外部的“角色状态存储箱”中状态值恢复给游戏角色
  /// </summary>
  /// <param name="memento"></param>
  public void RecoveryState(RoleStateMemento memento)
  {
    this.Vitality = memento.Vitality;
    this.Attack = memento.Attack;
    this.Defense = memento.Defense;
  }
}

角色状态存储箱类

/// <summary>
/// 角色状态存储箱
/// </summary>
class RoleStateMemento
{
  /// <summary>
  /// 将生命力、攻击力、防御力存入状态存储箱对象中
  /// </summary>
  /// <param name="vitality"></param>
  /// <param name="attack"></param>
  /// <param name="defense"></param>
  public RoleStateMemento(int vitality, int attack, int defense)
  {
    this.Vitality = vitality;
    this.Attack = attack;
    this.Defense = defense;
  }

  /// <summary>
  /// 生命力
  /// </summary>
  public int Vitality { get; set; }

  /// <summary>
  /// 攻击力
  /// </summary>
  public int Attack { get; set; }

  /// <summary>
  /// 防御力
  /// </summary>
  public int Defense { get; set; }
}

角色状态管理者类

/// <summary>
/// 角色状态管理者类
/// </summary>
class RoleStateCaretaker
{
  public RoleStateMemento Memento { get; set; }
}

客户端代码

static void Main(string[] args)
{
  // 大战Boss前
  GameRole lixiaoyao = new GameRole();

  // 游戏角色初始状态,三项指标数据都是100
  lixiaoyao.GetInitState();
  lixiaoyao.StateDisplay();

  // 保存进度
  RoleStateCaretaker stateAdmin = new RoleStateCaretaker();

  // 保存进度时,由于封装在Memento中,因此我们并不知道保存了哪些具体的角色数据
  stateAdmin.Memento = lixiaoyao.SaveState();

  // 大战Boss时,损耗严重
  // 开始大战Boss,三项指标数据都下降很多,非常糟糕,GameOver了
  lixiaoyao.Fight();
  lixiaoyao.StateDisplay();


  // 恢复之前状态
  // 不行,恢复保存的状态,重新来过
  lixiaoyao.RecoveryState(stateAdmin.Memento);
  lixiaoyao.StateDisplay();

  Console.Read();
}

“看看,能不能得满分,我查了好几遍了。”

“不错,写得还行。你要注意,备忘录模式也是有缺点的,角色状态需要完整存储到备忘录对象中,如果状态数据很大很多,那么在资源消耗上,备忘录对象会非常耗内存。”

“嗯,明白。所以也不是用得越多越好。”

“小子,以后打游戏要记着用备忘录哦。”大鸟不忘提醒一句。

“哈,我一定会这样。”小菜开始装着深沉地说,“曾经有一个精彩的游戏摆在我的面前,但是我没有好好珍惜。等到死于Boss手下的时候才后悔莫及,尘世间最痛苦的事莫过于此。如果上天可以给我一个机会再来一次的话,我会对你说三个字,‘存进度’。如果非要把这个进度加上一保险,我希望是刻成光盘,流传万年!”


每天学一点,不贪多。

下一篇我们接着读“第19章 分公司=一部门——组合模式”,欢迎关注微信公众号【乐趣课堂】。

原文出处:微信公众号【乐趣课堂】

原文链接:

本文观点不代表Dotnet9立场,转载请联系原作者。

发表评论

登录后才能评论