介绍

备忘录模式是一种行为模式,该模式用于保存对象的当前状态,并且可以在之后再次恢复到此状态,这有点像我们平常所说的“后悔药”。备忘录模式实现的方式需要保证被保存的对象状态不能被对象从外部访问,目的是为了保护好被保存的这些对象状态的完整性以及内部实现不向外暴露。

定义

在不被破坏封闭的前提下,捕获一个对象的内部状态,并在该对象之外保存这个状态,这样,以后就可以将该对象恢复到原先保存的状态。

使用场景

(1)需要保存一个对象在某一时刻的状态或部分状态。

(2)如果用一个接口来让其他对象得到这些状态,将会暴露对象的实现细节并破坏对象的封装性,一个对象不希望外界直接访问其内部状态,通过中间对象可以间接访问其内部状态。

备忘录模式的角色介绍

  • Originator:负责创建一个备忘录,可以记录、恢复自身的内部状态。同时Originator还可以根据需要决定Memento存储自身的哪些内部状态。
  • Memento:备忘录角色,用于存储Originator的内部状态,并且可以防止Originator以外的对象访问Memento。
  • Caretaker:负责存储备忘录,不能对备忘录的内容进行操作和访问,只能将备忘录传递给其他对象。

简单示例

备忘录模式比较贴切的场景应该是游戏中的存档功能,该功能就是将游戏进度存储到本地文件系统或者数据库中,下次再进入时从本地加载进度,使得玩家能够继续上一次的游戏之旅,这里我们就以“使命召唤”这款游戏为例简单演示下备忘录模式的实现。

首先,建立游戏类、备忘录类、Caretaker类,玩游戏到某个节点对游戏进行存档,然后退出游戏,再重新进入时从存档中读取进度,并且进入存档时的进度。

//使命召唤

public class CallOfDuty {
    private int mCheckPoint = 1;
    private int mLifeValue = 100;
    private String mWeapon = "沙漠之鹰";
    //玩游戏
    public void play() {
        System.out.println("玩游戏:" + String.format("第%关", mCheckPoint) + " 奋战杀敌中");
        mLifeValue -= 10;
        System.out.prinln("进度升级啦");
        mChcekPoint++;
        System.out.println("到达 " + String.format("第%关", mChcekPoint));
    }
    //退出游戏
    public void quit() {
        System.out.println("-------------");
        System.out.println("退出前的游戏属性:" + this.toString());
        System.out.println("退出游戏");
        System.out.println("-------------");

    }
    //创建备忘录
    public Memoto createMemoto() {
        Memoto memoto = new Memoto();
        memoto.mCheckPoint = mCheckPoint;
        memoto.mLifeValue = mLifeValue;
        memoto.mWeapon = mWeapon;
        return memoto;
    }
    //恢复游戏
    public void restore(Memoto memoto) {
        this.mCheckPoint = memoto.mCheckPoint;
        this.mLifeValue = memoto.mLifeValue;
        this.mWeapon = memoto.mWeapon;
        System.out.println("恢复后的游戏属性:" + this.toString());
    }
    //setter和getter省略。。。

    @override
    public String toString() {
        return "CallOfDuty [mCheckPoint=" + mCheckPoint + ", mLifeValue=" + mLifeValue + ", mWeapon=" + mWeapon + "]";
    }


}

在CallOfDuty游戏类中,我们存储了几个关键字段,关卡、人物的生命值、武器,当调用play函数玩游戏时,我们对关卡和人物的生命值进行修改。在该类中可以通过createMemoto函数来创建该用户的备忘录对象,也就是将自身的状态保存到一个Memoto对象中。外部可以通过restore函数将CallOfDuty对象的状态从备忘录对象中恢复。

我们先来看看备忘录对象,它只是存储CallOfDuty对象的字段:

//备忘录类
public class Memoto {
    public int mCheckPoint;
    public int mLifeValue;
    @Override
    public String toString() {
        return "CallOfDuty [mCheckPoint=" + mCheckPoint + ", mLifeValue=" + mLifeValue + ", mWeapon=" + mWeapon + "]";

    }
}

这就是一个无状态无操作的实体类,只负责用来存储Originator角色的一些数据,防止外部直接访问Originator。

而备忘录的操作者则是Caretaker角色:

//Caretaker,负责管理Memoto
public class Caretaker {
    //备忘录
    Memoto mMemoto;
    //存档
    public void archive(Memoto memoto) {
        this.mMemoto = memoto;
    }
    //获取存档
    public Memoto getMemoto() {
        return mMemoto;
    }
}

Caretaker类的职责很简单,就是负责管理Memoto对象,也就是备忘录对象。

public class Client {
    public static void main(String[] args) {
        //构建游戏对象
        CallOfDuty game = new CallOfDuty();
        //1、打游戏
        game.play();

        Caretaker caretaker = new Caretaker();
        //2、游戏存档
        caretaker.archive(game.createMemoto());
        //3、退出游戏
        game.quit();
        //4、恢复游戏
        CallOfDuty newGame = new CallOfDuty();


        newGame.restore(caretaker.getMemoto());

    }
}

上述过程大概有如下4步:

(1)开始游戏,闯关升级;

(2)游戏推出至前进行存档;

(3)退出游戏;

(4)重启游戏,从存档中恢复游戏进度。

CallOfDuty在这里为Originator角色,也就是需要存储数据的对象,在这里并没有直接存储CallOfDuty的对象,而是通过Memoto对CallOfDuty对象的数据进行存储,然后再存储Memoto对象,最终对Memoto的存取操作则交给Caretaker对象。在这个过程中,各个角色职责清晰、单一,代码也比较简单,代码也比较简单,即对外屏蔽了对CallOfDuty角色的直接访问,在满足了对象状态存取功能的同时也使得该模块的结构保持清晰整洁。

实战

下面我们开发一个简单的“记事本”应用,它的功能主要是记录想法。还有一些辅助功能如撤销、重做 等等。既然能随刻记录想法,自然想到了会用到备忘录模式。

首先,是布局xml:

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"

    <EditText
    android:id="@+id/note_edittext"
    android:layout_width="match_parent"
    android:layout_height="0dp"
    android:layout_weight="1"
    android:gravity="left"
    android:hint="@string/note_tips" />


    <RelativeLayout
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:marginBottom="20dp"
    android:paddingLeft="50dp"
    android:paddingRight="50dp" >

        <ImageView
        android:id="@+id/undo_btn"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_alignParentLeft="true"
        android:src="@drawable/undo"


        <TextView
        android:id="@+id/save_btn"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:centerInParent="true"
        android:text="@string/save"
        android:textSize="20sp" />


        <ImageView
        android:id="@+id/redo/btn"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_alignParentRight="true"
        android:src="@drawable/redo"

    </RelativeLayout>

</LinearLayout>

这是MainActivity中的布局,因此,相关的代码在MainActivity中,首先初始化View和点击事件:

//MainActivity
public class MainActivity extends Activity {
    //编辑框
    EditText mNodeEditText;
    //保存按钮
    TextView mSaveTv;
    //撤销按钮
    TextView mUndoBtn;
    //重做按钮
    ImageView mRedoBtn;


    //最大存储数量
    private static final int MAX = 30;
    //存储30条记录
    List<Memoto> mMemotos = new ArrayList<Memoto>(MAX);
    int mIndex = 0;


    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(saveInstanceState);
        setContentView(R.layout.activity_main);
        //初始化视图
        initViews();
    }
    private void initViews() {
        mNodeEditText = (EditText)findViewById(R.id.note_edittext);
        mUndoBtn = (ImageView)findViewById(R.id.undo_btn);
        mUndoBtn.setOnClickListener(new OnClickListener() {
            @Override
            public void onClick(View view) {
                //返回上一个记录点
                restoreEditText(getPrevMemoto());
                makeToast("撤销:");
            }
        });
        mRedoBtn = (ImageView)findViewById(R.id.redo_btn);
        mRedoBtn.setOnClickListener(new OnClickListener() {
            @Override
            public void onClick(View view) {
                //恢复状态,恢复到下一个记录点
                restoreEditText(getNextMemoto());
                makeToast("重做:");
            }
        });
        mSaveTv = (TextView)findViewById(R.id.save_btn);
        mSaveTv.setOnClickListener(new OnClickListener() {
            @Override
            public void onClick(View view) {
                //恢复状态,恢复到下一个记录点
                saveMemoto(createMemotoForEditText());
                makeToast("保存笔记:");
            }
        });

    }
    private void makeToast(String msgPrex) {
        Toast.makeText(this, msgPrex + mNodeEditText.getText() + ",光标位置:" + mNodeEditText.getSelectionStart(), Toast.LENGTH_LONG).show();
    }
    //省略代码。。。
}

当点击保存按钮时,会为编辑器创建一个Memoto对象用以保存编辑器的文本和光标位置,并且将这个Memoto存到一个列表中。当用户点击撤销时,从Memoto列表取出上一个记录,即恢复到上一个状态;用户点击重做按钮时,也就重复上一个撤消了的动作:

public class MainActivity extends Activity {
    //省略。。。
    ... ...
    //保存备忘录
    //@param memoto
    public void saveMemoto(Memoto memoto) {
        if(mMemoto.size() > MAX) {
            mMemoto.remove(0);
        }
        mMemoto.add(memoto);
        mIndex = mMemoto.size() - 1;

    }
    //获取上一个存档,相当于撤销功能
    public Memoto getPrevMemoto() {
        mIndex = mIndex > 0 ? --mIndex : mIndex;
        return mMemoto.get(mIndex);
    }
    //获取下一个存档,相当于重做功能
    public Memoto getNextMemoto() {
        mIndex = mIndex < mMemotos.size() - 1 ? ++mIndex : mIndex;
        return mMemoto.get(mIndex);
    }
    //为编辑器创建Memoto对象
    private Memoto createMemotoForEditText() {
        Memoto memoto = new Memoto();
        memoto.text = mNodeEditText.getText().toString();
        memoto.cursor = mNodeEditText.getSelectionStart();
    }
    //恢复编辑器状态
    private void restoreEditText(Memoto memoto) {
        mNodeEditText.setText(memoto.text);
        mNodeEditText.setSelection(memoto.cursor);
    }
}



存储EditText的文本与光标位置
public class Memoto {
    public String text;
    public int cursor;
}

上面的程序虽然完成了功能,但是MainActivity的职责太混乱了。既要负责View的部分逻辑,又要管理便签的记录、修改编辑器的状态,这样耦合性太强容易造成类型膨胀,后期难以维护,如果考虑使用备忘录模式,就可以使每个类的职责清晰:

//负责管理Memoto对象
public class NoteCaretaker {
    //最大存储数量
    private static final int MAX = 30;
    //存储30条记录
    List<Memoto> mMemotos = new ArrayList<Memoto>(MAX);


    int mIndex = 0;


    //保存备忘录到记录列表
    //@param memoto
    public void saveMemoto(Memoto memoto) {
        if(mMemotos.size() > MAX) {
            mMemotos.remove(0);
        }
        mMemotos.add(memoto);
        mIndex = mMemotos.size() - 1;
    }
    //获取上一个存档信息
    public Memoto getPrevMemoto() {
        mIndex = mIndex > 0 ? --mIndex : mIndex;
        return mMemotos.get(mIndex);
    }
    //获取下一个存档信息,相当于重做功能
    public Memoto getNextMemoto() {
        mIndex = mIndex < mMemotos.size() - 1 ? ++mIndex : mIndex;
    }
}

在NoteCaretaker 中会维护一个备忘录列表,然后使用mIndex标识编辑器当前所在的记录点,通过getPrevMemoto和getNextMemoto来获取上一个、下一个记录点的备忘录,以此来达到撤销和重做的功能。

然后自定义一个NodeEditText类,该类继承自EditText,具体代码如下:

public class NoteEditText extends EditText {
    public NoteEditText(Context context) {
        this(context, null);
    }
    public NoteEditText(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }
    public NoteEditText(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
    }
    //创建备忘录对象,即存储编辑器的指定数据
    public Memoto createMemoto() {
        Memoto noteMemoto = new Memoto();
        noteMemoto.text = getText().toString();
        noteMemoto.cursor = getSelectionStart();
        return noteMemoto;
    }
    //从备忘录中恢复数据
    public void restore(Memoto memoto) {
        setText(memoto.text);
        //设置光标位置
        setSelection(memoto.cursor);
    }

}

该类就是添加了两个函数,分别是createMemoto和restore函数。通过添加这两个函数。使得EditText能够管理自身的状态自信息,如果没有NodeEditText,那么createMemoto和restore函数的功能就需要放到其他的类型中。

最后是MainActivity:

//MainActivity
public class MainActivity extends Activity {
    //编辑框
    NoteEditText mNodeEditText;
    //保存按钮
    TextView mSaveTv;
    //撤销按钮
    TextView mUndoBtn;
    //重做按钮
    ImageView mRedoBtn;
    //备忘录管理器
    NoteCaretaker mCaretaker = new NoteCaretaker();


    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(saveInstanceState);
        setContentView(R.layout.activity_main);
        //初始化视图
        initViews();
    }
    private void initViews() {
        mNodeEditText = (EditText)findViewById(R.id.note_edittext);
        mUndoBtn = (ImageView)findViewById(R.id.undo_btn);
        mUndoBtn.setOnClickListener(new OnClickListener() {
            @Override
            public void onClick(View view) {
                //返回上一个记录点
                mNodeEditText.restore(mCaretaker.getPrevMemoto());
                makeToast("撤销:");
            }
        });
        mRedoBtn = (ImageView)findViewById(R.id.redo_btn);
        mRedoBtn.setOnClickListener(new OnClickListener() {
            @Override
            public void onClick(View view) {
                //恢复状态,恢复到下一个记录点
                mNodeEditText.restore(mCaretaker.getNextMemoto());
                makeToast("重做:");
            }
        });
        mSaveTv = (TextView)findViewById(R.id.save_btn);
        mSaveTv.setOnClickListener(new OnClickListener() {
            @Override
            public void onClick(View view) {
                //恢复状态,恢复到下一个记录点
                mCaretaker.saveMemoto(mNodeEditText.createMemoto());
                makeToast("保存笔记:");
            }
        });

    }
    private void makeToast(String msgPrex) {
        Toast.makeText(this, msgPrex + mNodeEditText.getText() + ",光标位置:" + mNodeEditText.getSelectionStart(), Toast.LENGTH_LONG).show();
    }

}

上述Activity中代码量少了很多,也简单了很多,职责更为单一,明确。

总结

备忘录模式是在不破坏封装的条件下,通过备忘录对象(Memoto)存储;另外一个对象内部状态的快照,在将来合适的时候把这个对象还原到存储起来的状态。

  • 优点:

(1)给用户提供了一种可以恢复状态的机制,可以使用户能够比较方便地回到某个历史的状态。

(2)实现了信息的封装,使得用户不需要关系状态的保存细节。

  • 缺点:

消耗资源,如果类的成员变量过多,势必会占用较大的资源,并且每一次保存都会消耗一定的内存。