Day 23: Behavioral patterns - Memento

目的

当系统需要提供「复原功能」、「取消复原功能」、「回复到上一个步骤」等需要将这些资料暂时存放在记忆体内,可以采纳的设计模式。

说明

要思考的是,在确保资料不会「任意被他者」复制、备份,且同时能有顺序地备份资料,供使用者想要「复原」时使用。

作法是:

  1. 定义好需要备份的物件(称作:Originator),这边可以采集中式一个物件或是多个小物件。
    1. 提供储存的方式,使用 Memento 物件达到此功能。
    2. 提供复原的方式,使用 Memento 物件达到此功能。
  2. 建立负责储存资料的物件(称作:Memento)
    1. 该物件的属性会跟 Originator 一致,确保没有资料遗漏。
    2. 该物件只会储存一次 Originator 的资料,如果要再次储存就重新实体化一个新的 Memento 物件。
  3. 建立负责储存多个 Memento 物件的看守者物件(称作:Caretaker),Originator 要执行储存、复原时,都是跟 Caretaker 沟通。

以下范例以「音乐模拟器」为核心制作。

UML 图

Memento Pattern UML Diagram

使用 Java 实作

制作将被储存的资料:Solfege

public class Solfege {
    private String name;

    public Solfege(String name) {
        this.name = name;
    }

    public String getName() {
        return name;
    }
}

制作音乐模拟器:MusicSimulator(Originator 物件)

public class MusicSimulator {
    private List<Solfege> inputs = new ArrayList<>();

    public void showInputs() {
        System.out.println("输入的音阶有:");

        for (Solfege solfege : inputs) {
            System.out.print(solfege.getName());
        }

        System.out.println("\n");
    }

    public void inputSolfege(Solfege solfege) {
        inputs.add(solfege);
    }

    public MusicSimulatorMemento saveInputs() {
        System.out.println("开始备份\n");
        StringBuilder stringBuilder = new StringBuilder();

        for (Solfege input : inputs) {
            stringBuilder.append(input.getName());
        }

        return new MusicSimulatorMemento(Base64.getEncoder().encodeToString(stringBuilder.toString().getBytes()));
    }

    public void restore(MusicSimulatorMemento musicSimulatorMemento) {
        System.out.println("开始还原\n");

        if (musicSimulatorMemento == null) {
            System.out.println("没有记录");
        } else {
            String decodedString = new String(Base64.getDecoder().decode(musicSimulatorMemento.getBackup().getBytes()));
            inputs.clear();

            for (String inputString : decodedString.split("")) {
                inputs.add(new Solfege(inputString));
            }
        }
    }
}

制作负责储存资料的物件:MusicSimulatorMemento(Memento物件)

public class MusicSimulatorMemento {
    private String backup;

    public MusicSimulatorMemento(String backup) {
        this.backup = backup;
    }

    public String getBackup() {
        return backup;
    }
}

制作建立负责储存多个 Memento 的物件:MusicSimulatorCareTaker(Caretaker物件)

public class MusicSimulatorCareTaker {
    private List<MusicSimulatorMemento> saves = new ArrayList<>();

    public MusicSimulatorMemento getUndo() {
        if (saves.isEmpty()) {
            return null;
        } else {
            return saves.get(saves.size() - 1);
        }
    }

    public void setSave(MusicSimulatorMemento memento) {
        saves.add(memento);
    }
}

测试,在模拟器上输入音符後,还原到上个状态:MusicSimulatorMementoSample

public class MusicSimulatorMementoSample {

    public static void main(String[] args) {
        MusicSimulator musicSimulator = new MusicSimulator();

        // 输入音阶
        musicSimulator.inputSolfege(new Solfege("C"));
        musicSimulator.inputSolfege(new Solfege("D"));
        musicSimulator.inputSolfege(new Solfege("E"));
        musicSimulator.inputSolfege(new Solfege("F"));

        // 确认输入的音阶
        musicSimulator.showInputs();

        // 储存
        MusicSimulatorCareTaker musicSimulatorCareTaker = new MusicSimulatorCareTaker();
        musicSimulatorCareTaker.setSave(musicSimulator.saveInputs());

        // 输入新的音阶
        musicSimulator.inputSolfege(new Solfege("G"));
        musicSimulator.inputSolfege(new Solfege("A"));
        musicSimulator.inputSolfege(new Solfege("B"));

        // 确认输入的音阶
        musicSimulator.showInputs();

        // 复原到上个状态
        musicSimulator.restore(musicSimulatorCareTaker.getUndo());

        // 确认输入的音阶
        musicSimulator.showInputs();
    }

}

使用 JavaScript 实作

制作将被储存的资料:Solfege

class Solfege {
  constructor(name) {
    /** @type {string} */
    this.name = name;
  }

  getName() {
    return this.name;
  }
}

制作音乐模拟器:MusicSimulator(Originator 物件)

class MusicSimulator {
  constructor() {
    /** @type {Solfege[]} */
    this.inputs = [];
  }

  showInputs() {
    console.log("输入的音阶有:");

    let solfegeList = ""

    for (const solfege of this.inputs) {
      solfegeList += solfege.getName();
    }

    console.log(`${solfegeList}\n`);
  }

  /** @param {Solfege} solfege */
  inputSolfege(solfege) {
    this.inputs.push(solfege);
  }

  saveInputs() {
    console.log("开始备份\n");

    let currentInputs = "";

    for (const solfege of this.inputs) {
      currentInputs += solfege.getName();
    }

    return new MusicSimulatorMemento(Buffer.from(currentInputs, 'utf8').toString('base64'));
  }

  /** @param {MusicSimulatorMemento} musicSimulatorMemento */
  restore(musicSimulatorMemento) {
    console.log("开始还原\n");

    if (musicSimulatorMemento === null) {
      console.log("没有记录");
    } else {
      const decodedString = Buffer.from(musicSimulatorMemento.getBackup(), 'base64').toString('utf8');
      this.inputs = [];

      for (const inputString of decodedString.split("")) {
        this.inputs.push(new Solfege(inputString));
      }
    }
  }
}

制作负责储存资料的物件:MusicSimulatorMemento(Memento物件)

class MusicSimulatorMemento {
  constructor(backup) {
    /** @type {string} */
    this.backup = backup;
  }

  getBackup() {
    return this.backup;
  }
}

制作建立负责储存多个 Memento 的物件:MusicSimulatorCareTaker(Caretaker物件)

class MusicSimulatorCareTaker {
  constructor() {
    /** @type {MusicSimulatorMemento[]} */
    this.saves = [];
  }

  getUndo() {
    if (this.saves.length === 0) {
      return null;
    } else {
      return this.saves[this.saves.length - 1];
    }
  }

  /** @param {MusicSimulatorMemento} */
  setSave(memento) {
    this.saves.push(memento);
  }
}

测试,在模拟器上输入音符後,还原到上个状态:MusicSimulatorMementoSample

const musicSimulatorMementoSample = () => {
  const musicSimulator = new MusicSimulator();

  // 输入音阶
  musicSimulator.inputSolfege(new Solfege("C"));
  musicSimulator.inputSolfege(new Solfege("D"));
  musicSimulator.inputSolfege(new Solfege("E"));
  musicSimulator.inputSolfege(new Solfege("F"));

  // 确认输入的音阶
  musicSimulator.showInputs();

  // 储存
  const musicSimulatorCareTaker = new MusicSimulatorCareTaker();
  musicSimulatorCareTaker.setSave(musicSimulator.saveInputs());

  // 输入新的音阶
  musicSimulator.inputSolfege(new Solfege("G"));
  musicSimulator.inputSolfege(new Solfege("A"));
  musicSimulator.inputSolfege(new Solfege("B"));

  // 确认输入的音阶
  musicSimulator.showInputs();

  // 复原到上个状态
  musicSimulator.restore(musicSimulatorCareTaker.getUndo());

  // 确认输入的音阶
  musicSimulator.showInputs();
}

musicSimulatorMementoSample();

总结

Memento 模式常见的译名是备忘录,实际的行为的确与备忘录相似,将资料「暂存」在某个地方,这边是建立物件暂存於记忆体内。也因为是建立在记忆体内,一旦累积大量的记录,会对记忆体造成不小的负担,有可能会让程序当掉。这让我想起很久以前 Word 会忽然当掉,如果没有存档,那所有文字都消失了。

我在测试时注意到,「复原功能」、「取消复原功能」需要一个 Array(JS)、List(Java)来储存,如果逻辑没有设定好,可能在记录的排序上出问题,这点在设计上要多加注意。

总之,这是个用於特殊情况下,同时在开发上的要注意的细节较多的模式。

明天将介绍 Behavioural patterns 的第七个模式:Observer 模式。


<<:  [Day23] Array methods 阵列操作方法(1)

>>:  Alpine Linux Porting (2.11) clock is _sorta_ ticking

【Day 24】用 SOLID 方式开发 React (1)

前言 在 OOP 的世界里,我们常常会听到高内聚(Cohesion),低耦合(Coupling),以...

[第一话] 一切的开始,web assembly

一睁开眼,发现出现在自己眼前的是没见过的景色 这里是哪里... 一阵晕眩过後 对了我想起来了,前一...

[DAY2]k8s在做什麽

先来张时代的眼泪 图片来源(官网资料) 最原始的实体主机一台一台设定环境:纯手工,因为硬体配置都是固...

大共享时代系列_025_迷你仓(共享仓储)

仓库被堆放了哪些遗忘的记忆呢? 哪些人在使用迷你仓呢? 对於地狭人绸的城市来说,这样的存放空间是必要...

数据分析的好夥伴 - Python基础:物件导向(下)

前面我们有说过,在Python的世界中,万物皆物件。但物件只是这个世界的最小单位而已,接下来让我们认...