CS-Notes全部笔记在阅读器中打开

备忘录(Memento)

意图

在不违反封装的情况下获得对象的内部状态,从而在需要时可以将对象恢复到之前保存的状态。

类图

备忘录对外提供两个接口:提供给 Caretaker 的窄接口,只能保存和传递备忘录,看不到其中的数据;提供给 Originator 的宽接口,可以读出恢复状态所需的所有数据。这样 Caretaker 保管备忘录时不依赖其中的数据,也不会破坏封装。

类图的配图

实现

以下实现了一个简单计算器程序,可以输入两个值,然后计算这两个值的和。备忘录模式允许将这两个值存储起来,然后在某个时刻用存储的状态进行恢复。

代码中的角色:CalculatorImp 是 Originator;PreviousCalculationToCareTaker 是窄接口,没有任何方法;PreviousCalculationToOriginator 是宽接口;PreviousCalculationImp 同时实现这两个接口;Client 是 Caretaker,它只拿到窄接口类型的备忘录。需要注意的是,这里的两个接口都是 public 的,Client 强转后仍能读出数据,隔离只是约定;要真正禁止读取,可以去掉公开的宽接口,把备忘录实现为 Originator 的私有静态嵌套类,只实现窄接口,由 Originator 在内部读取它的字段。

实现参考:Memento Pattern - Calculator Example - Java Sourcecode

/**
 * Originator 接口
 */
public interface Calculator {

    // 创建备忘录
    PreviousCalculationToCareTaker backupLastCalculation();

    // 用备忘录恢复状态
    void restorePreviousCalculation(PreviousCalculationToCareTaker memento);

    int getCalculationResult();

    void setFirstNumber(int firstNumber);

    void setSecondNumber(int secondNumber);
}
/**
 * Originator 实现
 */
public class CalculatorImp implements Calculator {

    private int firstNumber;
    private int secondNumber;

    @Override
    public PreviousCalculationToCareTaker backupLastCalculation() {
        // 创建保存两个数的备忘录
        return new PreviousCalculationImp(firstNumber, secondNumber);
    }

    @Override
    public void restorePreviousCalculation(PreviousCalculationToCareTaker memento) {
        this.firstNumber = ((PreviousCalculationToOriginator) memento).getFirstNumber();
        this.secondNumber = ((PreviousCalculationToOriginator) memento).getSecondNumber();
    }

    @Override
    public int getCalculationResult() {
        // 结果是两个数的和
        return firstNumber + secondNumber;
    }

    @Override
    public void setFirstNumber(int firstNumber) {
        this.firstNumber = firstNumber;
    }

    @Override
    public void setSecondNumber(int secondNumber) {
        this.secondNumber = secondNumber;
    }
}
/**
 * 提供给 Originator 的宽接口,用于恢复状态
 */
public interface PreviousCalculationToOriginator {
    int getFirstNumber();
    int getSecondNumber();
}
/**
 * 提供给 Caretaker 的窄接口
 */
public interface PreviousCalculationToCareTaker {
    // Caretaker 不能进行任何操作
}
/**
 * 备忘录实现,同时实现了宽接口和窄接口
 */
public class PreviousCalculationImp implements PreviousCalculationToCareTaker,
        PreviousCalculationToOriginator {

    private int firstNumber;
    private int secondNumber;

    public PreviousCalculationImp(int firstNumber, int secondNumber) {
        this.firstNumber = firstNumber;
        this.secondNumber = secondNumber;
    }

    @Override
    public int getFirstNumber() {
        return firstNumber;
    }

    @Override
    public int getSecondNumber() {
        return secondNumber;
    }
}
/**
 * Caretaker
 */
public class Client {

    public static void main(String[] args) {
        Calculator calculator = new CalculatorImp();

        // 用户输入两个数
        calculator.setFirstNumber(10);
        calculator.setSecondNumber(100);
        System.out.println(calculator.getCalculationResult());

        // 保存这次计算,以便出错时恢复
        PreviousCalculationToCareTaker memento = calculator.backupLastCalculation();

        // 用户输入了一个错误的数
        calculator.setFirstNumber(17);
        calculator.setSecondNumber(-290);
        System.out.println(calculator.getCalculationResult());

        // 用户按下 Ctrl + Z,恢复到上一次计算
        calculator.restorePreviousCalculation(memento);
        System.out.println(calculator.getCalculationResult());
    }
}
110
-273
110

JDK 中的应用

javax.swing.undo.StateEdit 用到了同样的思路:被编辑的对象实现 StateEditable,修改前后由对象把自己的状态写入 Hashtable,撤销或重做时再用它恢复。序列化可以用来把备忘录保存到文件中,但 Serializable 只是一个标记接口,本身不构成备忘录。

参考资料

在阅读器中可以用目录和搜索、切换深色模式、练习这篇笔记中的面试题:打开《设计模式 - 备忘录》