设计模式(7):行为型-备忘录模式(Memento)

设计模式(Design pattern)是一套被反复使用、多数人知晓的、经过分类编目的、代码设计经验的总结。使用设计模式是为了可重用代码、让代码更容易被他人理解、保证代码可靠性。 毫无疑问,设计模式于己于他人于系统都是多赢的;设计模式使代码编制真正工程化;设计模式是软件工程的基石脉络,如同大厦的结构一样。

设计模式分为三种类型,共23种。
创建型模式(5):单例模式、抽象工厂模式、建造者模式、工厂模式、原型模式。
结构型模式(7):适配器模式、桥接模式、装饰模式、组合模式、外观模式、享元模式、代理模式。
行为型模式(11):(父子类)策略模式、模版方法模式,(两个类)观察者模式、迭代器模式、职责链模式、命令模式,(类的状态)状态模式、备忘录模式,(中间类) 访问者模式、中介者模式、解释器模式。

一.概述

定义

  Memento(备忘录模式):在不破坏封装性的前提下,捕获一个对象的内部状态,并在该对象之外保存这个状态。这样以后就可将该对象恢复到保存的状态。 又叫快照模式(Snapshot Pattern)或Token模式。
  Memento:Without violating encapsulation, capture and externalize an object’s internal state so that the object can be restored to this state later.

结构

  备忘录模式的核心是备忘录类以及用于管理备忘录的负责人类的设计,其结构如图所示:

在备忘录模式结构图中包含如下几个角色:

  • Originator(原发器):它是一个普通类,可以创建一个备忘录,并存储它的当前内部状态,也可以使用备忘录来恢复其内部状态,一般将需要保存内部状态的类设计为原发器。
  • Memento(备忘录):存储原发器的内部状态,根据原发器来决定保存哪些内部状态。备忘录的设计一般可以参考原发器的设计,根据实际需要确定备忘录类中的属性。需要注意的是,除了原发器本身与负责人类之外,备忘录对象不能直接供其他类使用,原发器的设计在不同的编程语言中实现机制会有所不同。
  • Caretaker(负责人):负责人又称为管理者,它负责保存备忘录,但是不能对备忘录的内容进行操作或检查。在负责人类中可以存储一个或多个备忘录对象,它只负责存储对象,而不能修改对象,也无须知道对象的实现细节。

实现

理解备忘录模式并不难,但关键在于如何设计备忘录类和负责人类。由于在备忘录中存储的是原发器的中间状态,因此需要防止原发器以外的其他对象访问备忘录,特别是不允许其他对象来修改备忘录。典型代码如下:

// 备忘录类,默认可见性,包内可见
class Memento {
    private String state;

    public Memento(Originator o) {
        state = o.getState();
    }
    public void setState(String state) {
        this.state = state;
    }
    public String getState() {
        return this.state;
    }
}
//原发器
public class Originator {
    private String state;

    public Originator() {
    }

    // 创建一个备忘录对象
    public Memento createMemento() {    
        return new Memento(this);    
    }
    // 根据备忘录对象恢复原发器状态
    public void restoreMemento(Memento m) {    
        state = m.getState();    
    }

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

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

  在设计备忘录类时需要考虑其封装性,除了Originator类,不允许其他类来调用备忘录类Memento的构造函数与相关方法,如果不考虑封装性,允许其他类调用setState()等方法,将导致在备忘录中保存的历史状态发生改变,通过撤销操作所恢复的状态就不再是真实的历史状态,备忘录模式也就失去了本身的意义。
  在使用Java语言实现备忘录模式时,一般通过将Memento类与Originator类定义在同一个包(package)中来实现封装,在Java语言中可使用默认访问标识符来定义Memento类,即保证其包内可见(或使用内部类的方式)。只有Originator类可以对Memento进行访问,而限制了其他类对Memento的访问。在 Memento中保存了Originator的state值,如果Originator中的state值改变之后需撤销,可以通过调用它的restoreMemento()方法进行恢复。

//负责人
public class Caretaker {    
    private Memento memento;    

    public Memento getMemento() {    
        return memento;    
    }    

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

二.可悔棋的象棋

  Sunny软件公司欲开发一款可以运行在Android平台的触摸式中国象棋软件,由于考虑到有些用户是“菜鸟”,经常不小心走错棋;还有些用户因为不习惯使用手指在手机屏幕上拖动棋子,常常出现操作失误,因此该中国象棋软件要提供“悔棋”功能,用户走错棋或操作失误后可恢复到前一个步骤。
  这里写图片描述
Chessman充当原发器,ChessmanMemento充当备忘录,MementoCaretaker充当负责人,在MementoCaretaker中定义了一个ChessmanMemento类型的对象,用于存储备忘录。完整代码如下所示:

public class ChessClient {
    public static void main(String args[]) {
        MementoCaretaker mc = new MementoCaretaker();
        Chessman chess = new Chessman("车", 1, 1);
        display(chess);
        mc.setMemento(chess.save()); // 保存状态
        chess.setY(4);
        display(chess);
        mc.setMemento(chess.save()); // 保存状态
        display(chess);
        chess.setX(5);
        display(chess);
        System.out.println("******悔棋******");
        chess.restore(mc.getMemento()); // 恢复状态
        display(chess);
    }

    public static void display(Chessman chess) {
        System.out.println("棋子" + chess.getLabel() + "当前位置为:" 
                + "第" + chess.getX() + "行" + "第" + chess.getY() + "列。");
    }
}

class Chessman {
    private String label;
    private int x;
    private int y;

    public Chessman(String label, int x, int y) {
        this.label = label;
        this.x = x;
        this.y = y;
    }

    public void setLabel(String label) {
        this.label = label;
    }

    public void setX(int x) {
        this.x = x;
    }

    public void setY(int y) {
        this.y = y;
    }

    public String getLabel() {
        return (this.label);
    }

    public int getX() {
        return (this.x);
    }

    public int getY() {
        return (this.y);
    }

    // 保存状态
    public ChessmanMemento save() {
        return new ChessmanMemento(this.label, this.x, this.y);
    }

    // 恢复状态
    public void restore(ChessmanMemento memento) {
        this.label = memento.getLabel();
        this.x = memento.getX();
        this.y = memento.getY();
    }
}

// 象棋棋子备忘录类:备忘录
class ChessmanMemento {
    private String label;
    private int x;
    private int y;

    public ChessmanMemento(String label, int x, int y) {
        this.label = label;
        this.x = x;
        this.y = y;
    }

    public void setLabel(String label) {
        this.label = label;
    }

    public void setX(int x) {
        this.x = x;
    }

    public void setY(int y) {
        this.y = y;
    }

    public String getLabel() {
        return (this.label);
    }

    public int getX() {
        return (this.x);
    }

    public int getY() {
        return (this.y);
    }
}

// 象棋棋子备忘录管理类:负责人
class MementoCaretaker {
    private ChessmanMemento memento;

    public ChessmanMemento getMemento() {
        return memento;
    }

    public void setMemento(ChessmanMemento memento) {
        this.memento = memento;
    }
}

输出结果:

棋子车当前位置为:第1行第1列。
棋子车当前位置为:第1行第4列。
棋子车当前位置为:第1行第4列。
棋子车当前位置为:第5行第4列。
******悔棋******
棋子车当前位置为:第1行第4列。

实现多次撤销

  上述代码只能实现一次撤销,因为在负责人类中只定义一个备忘录对象来保存状态,后面保存的状态会将前一次保存的状态覆盖,但有时候用户需要撤销多步操作。如何实现多次撤销呢?本节将提供一种多次撤销的解决方案,那就是在负责人类中定义一个集合来存储多个备忘录,每个备忘录负责保存一个历史状态,在撤销时可以对备忘录集合进行逆向遍历,回到一个指定的历史状态,而且还可以对备忘录集合进行正向遍历,实现重做(Redo)操作,即取消撤销,让对象状态得到恢复。

修改MementoCaretaker类

class MementoCaretaker {    
    //定义一个集合来存储多个备忘录    
    private ArrayList mementolist = new ArrayList<>();    

    public ChessmanMemento getMemento(int i) {    
        return mementolist.get(i);    
    }    

    public void setMemento(ChessmanMemento memento) {    
        mementolist.add(memento);    
    }    
}    

重新编写客户端:

public class Client {    
    private static int index = -1; //定义一个索引来记录当前状态所在位置    
    private static MementoCaretaker mc = new MementoCaretaker();    

    public static void main(String args[]) {    
        Chessman chess = new Chessman("车",1,1);    
        play(chess);            
        chess.setY(4);    
        play(chess);    
        chess.setX(5);    
        play(chess);        
        undo(chess,index);    
        undo(chess,index);      
        redo(chess,index);    
        redo(chess,index);    
    }    

    //下棋    
    public static void play(Chessman chess) {    
        mc.setMemento(chess.save()); //保存备忘录    
        index ++;     
        System.out.println("棋子" + chess.getLabel() + "当前位置为:" 
        + "第" + chess.getX() + "行" + "第" + chess.getY() + "列。");    
    }    

    //悔棋    
    public static void undo(Chessman chess,int i) {    
        System.out.println("******悔棋******");    
        index --;     
        chess.restore(mc.getMemento(i-1)); //撤销到上一个备忘录    
        System.out.println("棋子" + chess.getLabel() + "当前位置为:"
         + "第" + chess.getX() + "行" + "第" + chess.getY() + "列。");    
    }    

    //撤销悔棋    
    public static void redo(Chessman chess,int i) {    
        System.out.println("******撤销悔棋******");     
        index ++;     
        chess.restore(mc.getMemento(i+1)); //恢复到下一个备忘录    
+ "第" + chess.getX() + "行" + "第" + chess.getY() + "列。");    
    }    
}   

输出结果如下:

棋子车当前位置为:第1行第1列。
棋子车当前位置为:第1行第4列。
棋子车当前位置为:第5行第4列。
******悔棋******
棋子车当前位置为:第1行第4列。
******悔棋******
棋子车当前位置为:第1行第1列。
******撤销悔棋******
棋子车当前位置为:第1行第4列。
******撤销悔棋******
棋子车当前位置为:第5行第4列。

拓展:本实例只能实现最简单的Undo和Redo操作,并未考虑对象状态在操作过程中出现分支的情况。如果在撤销到某个历史状态之后,用户再修改对象状态,此后执行Undo操作时可能会发生对象状态错误,大家可以思考其产生原因。【注:可将对象状态的改变绘制成一张树状图进行分析。】
在实际开发中,可以使用链表或者堆栈来处理有分支的对象状态改变,大家可通过链表或者堆栈对上述实例进行改进。

内部类实现

定义一个窄接口IChessmanMemento,这是一个标识接口,因此它没有定义出任何的方法。
修改Chessman的代码,将备忘录类定义为内部类:

public class Chessman {
    private String label;
    private int x;
    private int y;

    public Chessman(String label, int x, int y) {
        this.label = label;
        this.x = x;
        this.y = y;
    }

    public void setLabel(String label) {
        this.label = label;
    }

    public void setX(int x) {
        this.x = x;
    }

    public void setY(int y) {
        this.y = y;
    }

    public String getLabel() {
        return (this.label);
    }

    public int getX() {
        return (this.x);
    }

    public int getY() {
        return (this.y);
    }

    // 保存状态
    public IChessmanMemento save() {
        return new ChessmanMementoInner(this.label, this.x, this.y);
    }

    // 恢复状态
    public void restore(IChessmanMemento m) {
        ChessmanMementoInner memento = (ChessmanMementoInner)m;
        this.label = memento.getLabel();
        this.x = memento.getX();
        this.y = memento.getY();
    }
    //内部类实现
    private class ChessmanMementoInner implements IChessmanMemento{
        private String label;
        private int x;
        private int y;

        public ChessmanMementoInner(String label, int x, int y) {
            this.label = label;
            this.x = x;
            this.y = y;
        }
        public String getLabel() {
            return (this.label);
        }

        public int getX() {
            return (this.x);
        }

        public int getY() {
            return (this.y);
        }
    }
}

修改MementoCaretaker类:

class MementoCaretaker {
    private IChessmanMemento memento;

    public IChessmanMemento getMemento() {
        return memento;
    }

    public void setMemento(IChessmanMemento memento) {
        this.memento = memento;
    }
}

这样由于客户端无法访问到内部类ChessmanMementoInner,使得封装得到了保证。

三.总结

  备忘录模式在很多软件的使用过程中普遍存在,但是在应用软件开发中,它的使用频率并不太高,因为现在很多基于窗体和浏览器的应用软件并没有提供撤销操作。如果需要为软件提供撤销功能,备忘录模式无疑是一种很好的解决方案。在一些字处理软件、图像编辑软件、数据库管理系统等软件中备忘录模式都得到了很好的应用。

1.主要优点
(1)它提供了一种状态恢复的实现机制,使得用户可以方便地回到一个特定的历史步骤,当新的状态无效或者存在问题时,可以使用暂时存储起来的备忘录将状态复原。
(2)备忘录实现了对信息的封装,一个备忘录对象是一种原发器对象状态的表示,不会被其他代码所改动。备忘录保存了原发器的状态,采用列表、堆栈等集合来存储备忘录对象可以实现多次撤销操作。

2.主要缺点
  资源消耗过大,如果需要保存的原发器类的成员变量太多,就不可避免需要占用大量的存储空间,每保存一次对象的状态都需要消耗一定的系统资源。

3.适用场景
在以下情况下可以考虑使用备忘录模式:
(1)保存一个对象在某一个时刻的全部状态或部分状态,这样以后需要时它能够恢复到先前的状态,实现撤销操作。
(2)防止外界对象破坏一个对象历史状态的封装性,避免将对象历史状态的实现细节暴露给外界对象。

以上内容大部分来自于博文:JAVA设计模式(20):行为型-备忘录模式(Memento)
参考电子书下载:设计模式的艺术–软件开发人员内功修炼之道_刘伟(2013年).pdf

《道德经》第十章:
载营魄抱一,能无离乎?专气致柔,能如婴儿乎?涤除玄鉴,能无疵乎?爱民治国,能无为乎?天门开阖,能为雌乎?明白四达,能无知乎?生之畜之,生而不有,为而不恃,长而不宰,是谓玄德。
译文:精神和形体合一,能不分离吗?聚结精气以致柔和温顺,能像婴儿的无欲状态吗?清除杂念而深入观察心灵,能没有瑕疵吗?爱民治国能遵行自然无为的规律吗?感官与外界的对立变化相接触,能宁静吧?明白四达,能不用心机吗?让万事万物生长繁殖,产生万物、养育万物而不占为己有,作万物之长而不主宰他们,这就叫做“玄德”。

你可能感兴趣的:(设计模式,设计模式,备忘录模式,快照模式,Token模式,Memento)