备忘录(Memento)模式,也叫快照(Snapshot)模式,英文翻译是 Memento Design Pattern。在 GoF 的《设计模式》一书中,备忘录模式是这么定义的:

Captures and externalizes an object’s internal state so that it can be restored later, all without violating encapsulation.

翻译成中文就是:在不违背封装原则的前提下,捕获一个对象的内部状态,并在该对象之外保存这个状态,以便之后恢复对象为先前的状态。

我们通过一个例子来了解备忘录模式,相信大家应该都玩过一些一些含有存档的游戏吧,例如红警,魂斗罗等,每打完一个关卡后我们都会进行存档,防止下一关没有通过导致我们要从头开始。假设我们现在正在游戏的某个场景,游戏角色有生命力、攻击力、防御力等等数据,在打Boss前和后一定会不一样的,我们允许玩家如果感觉与Boss决斗的效果不理想可以让游戏恢复到决斗前。这个代码应该如何实现呢,最简单的我们可以新建一个类用来储存角色的生命力,攻击力,防御力等数据。代码如下:

<?php
namespace DesignPatterns\Memento\v1;

class GameRole
{

    private $vit;//生命力
    private $atk;//攻击力
    private $def;//防御力

    /**
     * @return mixed
     */
    public function getVit()
    {
        return $this->vit;
    }
    /**
     * @param mixed $vit
     */
    public function setVit($vit): void
    {
        $this->vit = $vit;
    }

    /**
     * @return mixed
     */
    public function getAtk()
    {
        return $this->atk;
    }
    /**
     * @param mixed $atk
     */
    public function setAtk($atk): void
    {
        $this->atk = $atk;
    }

    /**
     * @return mixed
     */
    public function getDef()
    {
        return $this->def;
    }
    /**
     * @param mixed $def
     */
    public function setDef($def): void
    {
        $this->def = $def;
    }

    //角色状态显示
    public function StateDisplay()
    {
        echo "角色当前的状态:<br>体力:".$this->vit,
             "<br>攻击力: ".$this->atk,"<br>防御力: ".$this->def."<br>";
    }

    //获得初始状态
    public function GetInitState(){
        $this->vit = 100;
        $this->atk = 100;
        $this->def = 100;
    }

    //战斗
    public function Fight(){
        $this->vit = 0;
        $this->atk = 0;
        $this->def = 0;
    }
}

运行Client.php

<?php
namespace DesignPatterns\Memento\v1;
require dirname(__DIR__).'/../vendor/autoload.php';
class Client
{
    public function run()
    {
        //大战boss前 获得初始角色状态
        $gameRole = new GameRole();
        $gameRole->GetInitState();
        $gameRole->StateDisplay();

        //保存进度  通过游戏角色的新实例,来保存进度
        $backup = new GameRole();
        $backup->setVit($gameRole->getVit());
        $backup->setAtk($gameRole->getAtk());
        $backup->setDef($gameRole->getDef());

        //大战boss损耗严重 所有数据损耗为0
        $gameRole->Fight();
        $gameRole->StateDisplay();

        //恢复之前状态
        $gameRole->setVit($backup->getVit());
        $gameRole->setAtk($backup->getAtk());
        $gameRole->setDef($backup->getDef());

        $gameRole->StateDisplay();


    }
}

$worker = new Client();
$worker->run();

运行结果:

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

  $backup = new GameRole();
  $backup->setVit($gameRole->getVit());
  $backup->setAtk($gameRole->getAtk());
  $backup->setDef($gameRole->getDef());
  $gameRole->setVit($backup->getVit());
  $gameRole->setAtk($backup->getAtk());
  $gameRole->setDef($backup->getDef());

显然,我们希望的是把这些‘游戏角色’的存取状态细节封装起来,而且最好是封装在外部的类当中。以体现职责分离。
这个就需要用到我们今天所要讲的备忘录模式了,我们一起来看看备忘录模式的结构与代码实现。

备忘录模式的结构与实现

结构

  • Originator (发起人):负责创建一个备忘录Memento,用以记录当前时刻它的内部状态,并可使用备忘录恢复内部状态。Originator 可根据需要决定Memento存储Originator的哪些内部状态。
  • Memento (备忘录):负责存储Originator对象的内部状态,并可防止Originator以外的其他对象访问备忘录Memento。备忘录有两个接口,Caretaker 只能看到备忘录的窄接口,它只能将备忘录传递给其他对象。Originator 能够看到一个宽接口,允许它访问返回到先前状态所需的所有数据。
  • Caretaker(管理者):负责保存好备忘录Memento,不能对备忘录的内容进行操作或检查。

就刚才的例子,‘ 游戏角色’类其实就是一个Originator,而我们用了同样的‘游戏角色’实例‘备份’来做备忘录,这在当需要保存全部信息时,是可以考虑的,而用clone的方式来实现Memento的状态保存可能是更好的办法,但是如果是这样的话,使得我们相当于对上层应用开放了Originator的全部( public)接口,这对于保存备份有时候是不合适的。
那如果我们不需要保存全部的信息以备使用时,怎么办? 这或许是很可能发生的情况,我们需要保存的如果并不是全部信息,而只是部分,那么就应该有一个独立的备忘录类Memento,它只拥有需要保存的信息的属性。

代码实现

Originator.php

<?php
namespace DesignPatterns\Memento\example;
/**
 * 发起人类
 * Class Originator
 * @package DesignPatterns\Memento\example
 */
class Originator
{
    private $state;

    /**
     * @param mixed $state
     */
    public function setState($state): void
    {
        $this->state = $state;
    }
    /**
     * @return mixed
     */
    public function getState()
    {
        return $this->state;
    }
    //创建备忘录,将当前需要保存的信息导入并实例化出一个Memento对象
    public function CreateMemento()
    {
      return  new Memento($this->state);
    }
    //恢复备忘录 将Memento导入并恢复相关数据
    public function setMemento(Memento $memento)
    {
        $this->state = $memento->getState();
    }
    //显示数据
    public function show()
    {
        echo 'State:'.$this->state."<br>";
    }
}

Memento.php

<?php
namespace DesignPatterns\Memento\example;
/**
 * 备忘录类
 * Class Memento
 * @package DesignPatterns\Memento\example
 */
class Memento
{
    private $state;
    //构造方法 将相关数据导入
    public function __construct($state)
    {
        $this->state = $state;
    }
    //需要保存的数据可能是多个
    /**
     * @param mixed $state
     */
    public function setState($state): void
    {
        $this->state = $state;
    }

    /**
     * @return mixed
     */
    public function getState()
    {
        return $this->state;
    }
}

Caretaker.php

<?php
namespace DesignPatterns\Memento\example;
/**
 * 管理者类
 * Class Caretaker
 * @package DesignPatterns\Memento\example
 */
class Caretaker
{
    private $memento;
    //获取或者设置备忘录
    /**
     * @return mixed
     */
    public function getMemento()
    {
        return $this->memento;
    }

    /**
     * @param mixed $memento
     */
    public function setMemento($memento): void
    {
        $this->memento = $memento;
    }
}

Client.php

<?php
namespace DesignPatterns\Memento\example;
require dirname(__DIR__).'/../vendor/autoload.php';
class Client
{
    public function run()
    {
        $originator = new Originator();
        $originator->setState('on');//Originator 初始状态为on
        $originator->show();

        //保存状态时,由于有了很好的封装,可以隐藏Originator的实现细节
        $caretaker = new Caretaker();
        $caretaker->setMemento($originator->CreateMemento());
        //更改状态属性为off
        $originator->setState('off');
        $originator->show();

        $originator->setMemento($caretaker->getMemento());
        $originator->show();



    }
}
$worker = new Client();
$worker->run();

运行结果

代码中把要保存的细节给封装在了Memento中了,如果以后哪天要更改保存的细节也不会影响客户端了。
Memento模式比较适用于功能比较复杂的,但需要维护或记录属性历史的类,或者需要保存的属性只是众多属性中的一小部分时,Originator 可以根据保存的Memento信息还原到前一状态。使用备忘录可以把复杂的对象内部信息对其他的对象屏蔽起来。
下面我们来使用备忘录模式把我们刚才的代码更改一下,代码如下:
GameRole.php

<?php
namespace DesignPatterns\Memento\v2;
/**
 * 游戏角色类
 * Class GameRole
 * @package DesignPatterns\Memento\v2
 */
class GameRole
{
    private $vit;//生命力
    private $atk;//攻击力
    private $def;//防御力

    /**
     * @return mixed
     */
    public function getVit()
    {
        return $this->vit;
    }
    /**
     * @param mixed $vit
     */
    public function setVit($vit): void
    {
        $this->vit = $vit;
    }

    /**
     * @return mixed
     */
    public function getAtk()
    {
        return $this->atk;
    }
    /**
     * @param mixed $atk
     */
    public function setAtk($atk): void
    {
        $this->atk = $atk;
    }

    /**
     * @return mixed
     */
    public function getDef()
    {
        return $this->def;
    }
    /**
     * @param mixed $def
     */
    public function setDef($def): void
    {
        $this->def = $def;
    }
    //获得初始状态
    public function GetInitState(){
        $this->vit = 100;
        $this->atk = 100;
        $this->def = 100;
    }

    //战斗
    public function Fight(){
        $this->vit = 0;
        $this->atk = 0;
        $this->def = 0;
    }
    //角色状态显示
    public function StateDisplay()
    {
        echo "角色当前的状态:<br>体力:".$this->vit,
            "<br>攻击力:".$this->atk,"<br>防御力:".$this->def."<br>";
    }

    //新增“保存角色状态”方法,将游戏角色的三个状态值通过实例化“角色状态存储箱”返回
    //保存角色状态
    public function SaveRoleState()
    {
        return new RoleStateMemento($this->vit,$this->atk,$this->def);
    }

    //恢复角色状态
    public function RecoveryState(RoleStateMemento $memento)
    {
       $this->vit = $memento->getVit();
       $this->atk = $memento->getAtk();
       $this->def = $memento->getDef();
    }
}

RoleStateMemento.php

<?php
namespace DesignPatterns\Memento\v2;
/**
 * 角色状态储存箱
 * Class RoleStateMemento
 * @package DesignPatterns\Memento\v2
 */
class RoleStateMemento
{
    private $vit;//生命力
    private $atk;//攻击力
    private $def;//防御力

    public function __construct($vit,$atk,$def)
    {
        $this->vit = $vit;
        $this->atk = $atk;
        $this->def = $def;
    }

    /**
     * @param mixed $vit
     */
    public function setVit($vit): void
    {
        $this->vit = $vit;
    }

    /**
     * @return mixed
     */
    public function getVit()
    {
        return $this->vit;
    }

    /**
     * @param mixed $atk
     */
    public function setAtk($atk): void
    {
        $this->atk = $atk;
    }

    /**
     * @return mixed
     */
    public function getAtk()
    {
        return $this->atk;
    }

    /**
     * @param mixed $def
     */
    public function setDef($def): void
    {
        $this->def = $def;
    }

    /**
     * @return mixed
     */
    public function getDef()
    {
        return $this->def;
    }


}

RoleStateCaretaker.php

<?php
namespace DesignPatterns\Memento\v2;
/**
 * 角色状态管理者类
 * Class RoleStateCaretaker
 * @package DesignPatterns\Memento\v2
 */
class RoleStateCaretaker
{
    private $memento;

    /**
     * @param mixed $memento
     */
    public function setMemento($memento): void
    {
        $this->memento = $memento;
    }

    /**
     * @return mixed
     */
    public function getMemento()
    {
        return $this->memento;
    }
}

Client.php

<?php
namespace DesignPatterns\Memento\v2;
require dirname(__DIR__).'/../vendor/autoload.php';
class Client
{
    public function run()
    {

        $gameRole = new GameRole();
        //游戏角色初始状态,三项数据都是100
        $gameRole->GetInitState();
        $gameRole->StateDisplay();

        //保存进度
        //保存进度时,由于封装在Memento中,因此我们并不知道保存了哪些具体的角色数据
        $RoleStateCaretaker = new RoleStateCaretaker();
        $RoleStateCaretaker->setMemento($gameRole->SaveRoleState());

        //大战boss,损失严重
        $gameRole->Fight();
        $gameRole->StateDisplay();

        //恢复之前的状态
        $gameRole->RecoveryState($RoleStateCaretaker->getMemento());
        $gameRole->StateDisplay();

    }
}

$worker = new Client();
$worker->run();

运行结果:

总结

备忘录模式也叫快照模式,具体来说,就是在不违背封装原则的前提下,捕获一个对象的内部状态,并在该对象之外保存这个状态,以便之后恢复对象为先前的状态。备忘录模式将代码中把要保存的细节给封装在了Memento中了,如果以后哪天要更改保存的细节也不会影响客户端了。
备忘录模式比较适用于功能比较复杂的,但需要维护或记录属性历史的类,或者需要保存的属性只是众多属性中的一小部分时,Originator 可以根据保存的Memento信息还原到前一状态。使用备忘录可以把复杂的对象内部信息对其他的对象屏蔽起来。

github示例:https://github.com/yangpanyao/design-patterns/tree/master/Memento

Last modification:July 17th, 2020 at 10:26 pm