备忘录模式

简介

Without violating encapsulation, capture and externalize an object's internal state so that the object can be restored to this state later.
在不破坏封装的前提下,捕获一个对象的内部状态,并在对象之外保存这个状态。这样以后就可将该对象恢复到原先保存的状态。

备忘录模式(Memento Pattern)又称为 快照模式(Snapshot Pattern)Token模式

在软件系统中,备忘录模式 为我们提供一种 “后悔药” 的机制,它通过存储系统各个历史状态的快照,使得我们可以在任一时刻将系统回滚到某一个历史状态。

对于 备忘录模式 来说,比较贴切的现实场景应该是游戏的存档功能,通过将游戏当前进度存储到本地文件系统或数据库中,使得下次继续游戏时,玩家可以从之前的位置继续进行。

备忘录模式 本质:从发起人实体类(Originator)隔离存储功能,降低实体类的职责。同时由于存储信息(Memento)独立,且存储信息的实体交由管理类(Caretaker)管理,则可以通过为管理类扩展额外的功能对存储信息进行扩展操作(比如增加历史快照功能···)。

主要解决

存储实体(Originator)状态,存储历史快照,回滚历史状态。

优缺点

优点

  • 简化发起人实体类(Originator)职责,隔离状态存储与获取,实现了信息的封装,客户端无需关心状态的保存细节;
  • 提供状态回滚功能;

缺点

  • 消耗资源:如果需要保存的状态过多时,每一次保存都会消耗很多内存;

使用场景

  • 需要保存历史快照的场景;
  • 希望在对象之外保存状态,且除了自己其他类对象无法访问状态保存具体内容;

模式讲解

首先来看下 备忘录模式 的通用 UML 类图:

备忘录模式_第1张图片
备忘录模式

从 UML 类图中,我们可以看到,备忘录模式 主要包含三种角色:

  • 发起人角色(Originator):负责创建一个备忘录,记录自身需要保存的状态;具备状态回滚功能;
  • 备忘录角色(Memento):用于存储 Originator 的内部状态,且可以防止 Originator 以外的对象进行访问;
  • 备忘录管理员角色(Caretaker):负责存储,提供,管理备忘录(Memento),无法对备忘录内容进行操作和访问;

以下是 备忘录模式 的通用代码:

class Client {
    public static void main(String[] args) {
        //来一个发起人
        Originator originator = new Originator();
        //来一个备忘录管理员
        Caretaker caretaker = new Caretaker();
        //管理员存储发起人的备忘录
        caretaker.storeMemento(originator.createMemento());
        //发起人从管理员获取备忘录进行回滚
        originator.restoreMemento(caretaker.getMemento());

    }

    static class Memento {
        private String state;

        public Memento(String state){
            this.state = state;
        }

        public String getState() {
            return this.state;
        }

        public void setState(String state) {
            this.state = state;
        }
    }

    static class Caretaker {
        // 备忘录对象
        private Memento memento;

        public Memento getMemento() {
            return this.memento;
        }

        public void storeMemento(Memento memento) {
            this.memento = memento;
        }

    }

    static class Originator {
        // 内部状态
        private String state;

        public String getState() {
            return this.state;
        }

        public void setState(String state) {
            this.state = state;
        }

        // 创建一个备忘录
        public Memento createMemento() {
            return new Memento(this.state);
        }

        // 从备忘录恢复
        public void restoreMemento(Memento memento) {
            this.setState(memento.getState());
        }
    }
}

:备忘录模式 要求备忘录(Memento)只对发起人(Originator)内容可见,对其他对象(Caretaker)内容不可见;但是在上面代码中,备忘录管理员(Caretaker)其实是可以通过备忘录(Memento)提供的相关方法(getStatesetState)获取和修改内部状态,这违背了 备忘录模式 的要求,可能造成备忘录内部状态无意间被其他对象修改,导致发起人状态恢复错误,系统稳定性下降。

修复上述问题的代码如下所示:

interface IMemento {
}

static class Caretaker {
    // 备忘录对象
    private IMemento memento;

    public IMemento getMemento() {
        return this.memento;
    }

    public void storeMemento(IMemento memento) {
        this.memento = memento;
    }

}

static class Originator {
    // 内部状态
    private String state;
    ...
    ...
    // 从备忘录恢复
    public void restoreMemento(IMemento memento) {
        this.state = ((Memento) memento).state;
    }

    p'rstatic class Memento implements IMemento {
        private String state;

        private Memento(String state) {
            this.state = state;
        }
    }
}

从上面的代码中我们可以看到:我们把Memento作为Originator的私有静态内部类,这样就满足了OriginatorMemento具有宽访问权限,但是直接这样做,其他类(Caretaker)是无法访问Memento的,因此在最上面我们通过定义一个空接口IMemento,然后让Memento实现IMemento,变相将Memento扩展为公有IMemento,这样就实现了其他类(Caretaker)对Memento具备窄访问权限。这种形式才算是严格满足 备忘录模式 的设计要求。

举个例子

例子:假设现有一个游戏,该游戏规定,对于之前通关的关卡,可以随时跳回到任一关卡继续游戏。也就是说,如果你现在已经在第30关卡,那么30关卡之前的任一一关,你可以随时切回去玩。请用代码实现上述游戏逻辑。

分析:对于已通过的关卡,可以随时切换回去玩,那么系统肯定是对已通过的关卡做了备份,对于通关的每一关卡的相关内容都进行了存储(为了简单,我们只认为对关卡名字,关卡通关时间做了备份),那么我们只需实现存储功能与回滚功能即可。

具体代码如下:

class Client {
    public static void main(String[] args) {
        GameCaretaker caretaker = new GameCaretaker();
        Game game = new Game();
        System.out.println(game);
        caretaker.addSnapshot(game.createGameInfo());

        game.doneChapter("002", 20);
        System.out.println(game);
        caretaker.addSnapshot(game.createGameInfo());

        game.doneChapter("003", 30);
        System.out.println(game);
        caretaker.addSnapshot(game.createGameInfo());

        game.restore(caretaker.getSnapshot("002"));
        System.out.println("rollback to chapter 002");
        System.out.println(game);
    }

    // 接口:IMemento
    interface IGameInfo {
        // 返回关卡名称
        String name();
    }

    //Caretaker
    static class GameCaretaker {
        private Map mGameSnapshots = new HashMap<>();

        public void addSnapshot(IGameInfo snapshot) {
            this.mGameSnapshots.put(snapshot.name(), snapshot);
        }

        public IGameInfo getSnapshot(String name) {
            return this.mGameSnapshots.getOrDefault(name, null);
        }
    }

    //Originator
    static class Game {
        // 关卡时间
        private String mName;
        // 通关时间
        private int mCost;

        public Game() {
            this.mName = "001";
            this.mCost = 10;
        }

        public Game(String name, int cost) {
            this.mName = name;
            this.mCost = cost;
        }

        public void doneChapter(String name, int cost) {
            this.setName(name);
            this.setCost(cost);
        }

        private void setName(String name) {
            this.mName = name;
        }

        private void setCost(int cost) {
            this.mCost = cost;
        }

        public IGameInfo createGameInfo() {
            return new GameInfoStore(this.mName, this.mCost);
        }

        public void restore(IGameInfo info) {
            if (info == null) {
                throw new IllegalArgumentException("Game Snapshot is empty!!");
            }
            GameInfoStore game = (GameInfoStore) info;
            this.mName = game.name;
            this.mCost = game.cost;
        }

        @Override
        public String toString() {
            return String.format("Game[%s] cost you: %d mins", this.mName, this.mCost);
        }

        //Memento
        private static class GameInfoStore implements IGameInfo {
            private String name;
            private int cost;

            private GameInfoStore(String name, int cost) {
                this.name = name;
                this.cost = cost;
            }

            @Override
            public String name() {
                return this.name;
            }
        }
    }
}

我们通过为GameCaretaker添加一个集合记录游戏关卡各个记录,使整个系统具备回滚功能。

运行结果如下:

Game[001] cost you: 10 mins
Game[002] cost you: 20 mins
Game[003] cost you: 30 mins
rollback to chapter 002
Game[002] cost you: 20 mins

你可能感兴趣的:(备忘录模式)