Files
knowledge-kit/Chapter6 - Design Pattern/6.27.md
2023-11-02 16:06:38 +08:00

7.4 KiB
Raw Blame History

备忘录模式

对于大对象的备份和恢复,如何优化内存和时间的消耗

定义

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

Captures and externalizes an objects internal state so that it can be restored later, all without violating encapsulation. 翻译成中文就是:在不违背封装原则的前提下,捕获一个对象的内部状态,并在该对象之外保存这个状态,以便之后恢复对象为先前的状态 核心就是2点

  • 存储副本以便后期恢复
  • 在不违背封装原则的前提下,进行对象的备份和恢复

如何实现

  • 为什么存储和恢复副本会违背封装原则?
  • 备忘录模式是如何做到不违背封装原则的?

编写一个小程序,可以接收命令行的输入。用户输入文本时,程序将其追加存储在内存文本中;用户输入“:list”程序在命令行中输出内存文本的内容用户输入“:undo”程序会撤销上一次输入的文本也就是从内存文本中将上次输入的文本删除掉。

>hello
>:list
hello
>world
>:list
helloworld
>:undo
>:list
hello

简易版本

public class InputText {
    private StringBuilder text = new StringBuilder();
    public String getText() {
        return text.toString();
    }
    public void append(String input) {
        text.append(input);
    }
    public void setText(String text) {
        this.text.replace(0, this.text.length(), text);
    }
}
public class SnapshotHolder {
    private Stack<InputText> snapshots = new Stack<>();
    public InputText popSnapshot() {
        return snapshots.pop();
    }
    public void pushSnapshot(InputText inputText) {
        InputText deepClonedInputText = new InputText();
        deepClonedInputText.setText(inputText.getText());
        snapshots.push(deepClonedInputText);
    }
}

public class ApplicationMain {
    public static void main(String[] args) {
        InputText inputText = new InputText();
        SnapshotHolder snapshotsHolder = new SnapshotHolder();
        Scanner scanner = new Scanner(System.in);
        while (scanner.hasNext()) {
            String input = scanner.next();
            if (input.equals(":list")) {
                System.out.println(inputText.toString());
            } else if (input.equals(":undo")) {
                InputText snapshot = snapshotsHolder.popSnapshot();
                inputText.setText(snapshot.getText());
            } else {
                snapshotsHolder.pushSnapshot(inputText);
                inputText.append(input);
            }
        }
    }
}

问题分析:仔细想想遵循备忘录模式吗(要在不违背封装原则的前提下,进行对象的备份和恢复)?

  • 为了实现快照恢复功能,在 InputText 类中定义了 setText 方法,这个方法名字很普通,就是一个设置文本的方法,而且还是公有方法,外部很容易误调用,也不符合封装原则
  • 快照应该是不可变的,所以不需要对外暴露 set 等用于修改内部状态的方法。且上面的实现复用了 InputText 这个类用于充当快照类。InputText 同时存在一系列修改内部状态的方法,违背封装原则

改进:

  • 单独抽取 SnapShot 类,用于快照数据的保存,对外只暴露 get 方法,没有 set 方法去修改内部状态
  • Input 类中,把 setText 改为针对特定场景的 resetSnapShot 方法,标识特定场景,只用于恢复快照数据
public class InputText {
    private StringBuilder text = new StringBuilder();
    public String getText() {
        return text.toString();
    }
    public void append(String input) {
        text.append(input);
    }
    public Snapshot createSnapshot() {
        return new Snapshot(text.toString());
    }
    public void restoreSnapshot(Snapshot snapshot) {
        this.text.replace(0, this.text.length(), snapshot.getText());
    }
}

public class Snapshot {
    private String text;
    public Snapshot(String text) {
        this.text = text;
    }
    public String getText() {
        return this.text;
    }
}

public class SnapshotHolder {
    private Stack<Snapshot> snapshots = new Stack<>();
    public Snapshot popSnapshot() {
        return snapshots.pop();
    }
    public void pushSnapshot(Snapshot snapshot) {
        snapshots.push(snapshot);
    }
}

public class ApplicationMain {
    public static void main(String[] args) {
        InputText inputText = new InputText();
        SnapshotHolder snapshotsHolder = new SnapshotHolder();
        Scanner scanner = new Scanner(System.in);
        while (scanner.hasNext()) {
            String input = scanner.next();
            if (input.equals(":list")) {
                System.out.println(inputText.toString());
            } else if (input.equals(":undo")) {
                Snapshot snapshot = snapshotsHolder.popSnapshot();
                inputText.restoreSnapshot(snapshot);
            } else {
            snapshotsHolder.pushSnapshot(inputText.createSnapshot());
                inputText.append(input);
            }
        }
    }
}

上面的代码实现就是典型的备忘录模式的代码实现,也是很多书籍(包括 GoF 的《设计模式》)中给出的实现方法。

QA

备忘录模式,还有一个跟它很类似的概念,“备份”,有何异同? 两者的应用场景很类似,都应用在防丢失、恢复、撤销等场景中。备忘录模式更侧重于代码的设计和实现,备份更侧重架构设计或产品设计

优化内存和时间消耗

上面的实现存在问题:如果要备份的对象数据比较大,备份频率又比较高,那快照占用的内存会比较大,备份和恢复的耗时会比较长。这个问题该如何解决呢?

不同的应用场景下有不同的解决方法。比如,我们前面举的那个例子,应用场景是利用备忘录来实现撤销操作,而且仅仅支持顺序撤销,也就是说,每次操作只能撤销上一次的输入,不能跳过上次输入撤销之前的输入。在具有这样特点的应用场景下,为了节省内存,我们不需要在快照中存储完整的文本,只需要记录少许信息,比如在获取快照当下的文本长度,用这个值结合 InputText 类对象存储的文本来做撤销操作。

我们再举一个例子。假设每当有数据改动,我们都需要生成一个备份,以备之后恢复。如果需要备份的数据很大,这样高频率的备份,不管是对存储(内存或者硬盘)的消耗,还是对 时间的消耗,都可能是无法接受的。想要解决这个问题,我们一般会采用“低频率全量备份”和“高频率增量备份”相结合的方法。

全量备份就不用讲了,它跟我们上面的例子类似,就是把所有的数据“拍个快照”保存下来。所谓“增量备份”,指的是记录每次操作或数据变动。

当我们需要恢复到某一时间点的备份的时候,如果这一时间点有做全量备份,我们直接拿来恢复就可以了。如果这一时间点没有对应的全量备份,我们就先找到最近的一次全量备份, 然后用它来恢复,之后执行此次全量备份跟这一时间点之间的所有增量备份,也就是对应的操作或者数据变动。这样就能减少全量备份的数量和频率,减少对时间、内存的消耗。