10k

设计模式之美-课程笔记43备忘录模式

备忘录模式:优化大对象备份和恢复的内存和时间消耗

原理与实现

  1. 也叫快照模式(Memento Design Pattern): Captures and externalizes and object's internal state so that it can be restored later, all without violating encapsulation.
  2. 不违背封装,捕获一个对象的内部状态,并在该对象之外保存这个状态,以便之后恢复对象为先前的状态。
  3. 希望你编写一个小程序,可以接收命令行的输入。用户输入文本时,程序将其追加存储在内存文本中;用户输入“:list”,程序在命令行中输出内存文本的内容;用户输入“:undo”,程序会撤销上一次输入的文本,也就是从内存文本中将上次输入的文本删除掉。
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.Scanner;

/**
 * Created by zz on 2023/11/5
 */
public class Memo {
    private static List<Integer> offsets = new ArrayList<>();
    private final static String fileName = "/Users/szhang/Project75/src/text.txt";

    public static void main(String[] args) throws IOException {
        String contents = new BufferedReader(new FileReader(fileName)).readLine();
        while (true) {
            try {
                // keep scanning input from user;
                String reader = new Scanner(System.in).nextLine();
                // if input is (:list), then print out file content;
                if (":list".equals(reader)) {
                    System.out.println(contents);
                } else if (":undo".equals(reader)) {
                    int offset = offsets.get(offsets.size() - 1);
                    offsets.remove(offsets.size() - 1);
                    contents = contents.substring(0, contents.length() - offset);
                    BufferedWriter writer = new BufferedWriter(new FileWriter(fileName));
                    writer.write(contents);
                } else {
                    String newContent = reader;
                    offsets.add(newContent.length());
                    contents += newContent;
                    BufferedWriter writer = new BufferedWriter(new FileWriter(fileName));
                    writer.write(contents);
                }
            } catch (Exception ignored) {

            }
            // if input is (:undo), then remove last time input and print out file the latest content;
            // use an offset to keep tracking previous added string length;
        }
    }
}
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.getText());
      } else if (input.equals(":undo")) {
        InputText snapshot = snapshotsHolder.popSnapshot();
        inputText.setText(snapshot.getText());
      } else {
        snapshotsHolder.pushSnapshot(inputText);
        inputText.append(input);
      }
    }
  }
}

上面的代码的问题在于:

  1. InputText中有setText方法,可能会被其他业务调用,违背封装。
  2. 快照在理论上说是一个不可变对象,但是snapshot类复用了inputText(包含内部修改方法)。违背封装。

针对以上两点,可以:

  1. 将setText方法重命名为restoreSnapshot指意更明确;
  2. 定义独立的snapshot类不复用inputText
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);
      }
    }
  }
}

和我最开始的那个实现的区别在于:

  1. 不用offset,因为offset会频繁读写文件,还是基于文件的内容,而snapshot相当于新加了一个数据结构来存储每一个版本,可以当真正需要最后一步关闭的时候再去将最新的内容写进去即可。
  2. 不破坏封装。通过多了一层数据结构(snapshot类),将包含直接修改内容的数据操作与容器分类开来。减少风险。

如何优化内存和时间消耗

不同场景需求不同策略

  1. 如果是之前需求,顺序操作,那么不需要再容器中存完整文本,只需要存一个长度信息即可,最后再结合inputText的文本内容操作。
  2. 如果我们的需求是备份在每一个改动,那这是个高频操作,消耗内存、时间,那采取了一个策略是高频增量备份和低频全量备份结合的方式。
    1. 需要恢复的时候如果那个时间点有全量备份直接恢复;
    2. 如果没有全量,找最近时间的全量然后根据增量备份一点点恢复到这个时间点的全量内容。
Thoughts? Leave a comment