• 备忘录模式


    如果大家觉得文章有错误内容,欢迎留言或者私信讨论~

      备忘录模式理解与掌握不难,实现比较灵活,应用场景也比较明确和有限,主要用来是防丢失、撤销和恢复等。话不多说,让我们开始吧。

    原理和实现

    Captures and externalizes an object’s internal state so that it can be restored later, all without violating encapsulation.

      备忘录模式,也称为快照模式。上面这段从 GOF 的《设计模式》摘录下来的解释翻译为中文就是:在不违背封装原则的前提下,捕获一个对象的内部状态,并在该对象之外保存这个状态,以便之后恢复对象为先前的状态。
      这个模式主要表达两个部分。第一部分就是存储副本方便以后恢复,这个好理解。另一部分是,在不违背封装原则的前提下,进行对象的备份和恢复。 在这部分我们需要理清 2 个问题:

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

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

    >hello
    >:list
    hello
    >world
    >:list
    helloworld
    >:undo
    >:list
    hello
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

      怎样去实现?方法肯定有好多种,你可以自己先打开 IDEA 尝试一下,备忘录的实现方式灵活。等你实现完成后可以看一下下面这种实现方式:

    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);
    			}
    		}
    	}
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47

      上面的代码已经实现了第一部分的备忘录功能。但是,如果我们深究一下的话,还有一些问题要解决,那就是前面定义中提到的第二点:要在不违背封装原则的前提下,进行对象的备份和恢复。而上面的代码并不满足这一点,主要体现在下面两方面:

    • 为了能用快照恢复 InputText 对象,我们在 InputText 类中定义了 setText() 函数,但这个函数有可能会被其他业务使用,所以,暴露不应该暴露的函数违背了封装原则;
    • 快照本身是不可变的,理论上讲,不应该包含任何 set() 等修改内部状态的函数,但在上面的代码实现中,“快照“这个业务模型复用了 InputText 类的定义,而InputText 类本身有一系列修改内部状态的函数,所以,用 InputText 类来表示快照违背了封装原则。

      针对以上问题,我们对代码做两点修改。其一,定义一个独立的类(Snapshot 类)来表示快照,而不是复用 InputText 类。这个类只暴露 get() 方法,没有 set() 等任何修改内部状态的方法。其二,在 InputText 类中,我们把 setText() 方法重命名为 restoreSnapshot()方法,用意更加明确,只用来恢复对象。

      按照这个思路我们来重构上面的代码:

    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
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61

      上面这段代码就是《设计模式》中的经典写法。

    如何优化内存和时间消耗

      前面我们只是简单介绍了备忘录模式的原理和经典实现,现在我们再继续深挖一下。如果要备份的对象数据比较大,备份频率又比较高,那快照占用的内存会比较大,备份和恢复的耗时会比较长。这个问题该如何解决呢?
      不同场景下有不同的做法,比如上面的撤销动作,我们是存储了整条文本,假设我们支持顺序撤销,那么在这种场景下,我们不需要在快照中存储完整的文本,只需要记录少许信息,比如在获取快照当下的文本长度,用这个值结合 InputText 类对象存储的文本来做撤销操作。
      我们再举一个例子。假设每当有数据改动,我们都需要生成一个备份,以备之后恢复。如果需要备份的数据很大,这样高频率的备份,不管是对存储(内存或者硬盘)的消耗,还是对时间的消耗,都可能是无法接受的。想要解决这个问题,我们一般会采用“低频率全量备份”和“高频率增量备份”相结合的方法(这里实际上可以是 Redis 的快照存储模式)。
      全量备份就不用讲了,它跟我们上面的例子类似,就是把所有的数据“拍个快照”保存下
    来。所谓“增量备份”,指的是记录每次操作或数据变动。

  • 相关阅读:
    Java中的队列Queue
    一次性插入一千个 ​<li>​元素到一个 ​<ul>​标签中
    新店速递|白玉兰(商务)酒店福州火车站西湖长冠店 正式上线
    招投标系统软件源码,招投标全流程在线化管理
    BFO:Big Faceless PDF Library for JAVA
    看我如何连夜自建网站背刺我的求职对手们
    【完美世界】战王之殇特别篇定档,11月3日播,云曦受辱石昊杀红眼了
    netty系列之:可以自动通知执行结果的Future,有见过吗?
    Qt元对象系统:QMetaMethod
    在 kubernetes 环境中实现 gRPC 负载均衡
  • 原文地址:https://blog.csdn.net/qq_43654226/article/details/126870641