龙空技术网

一文彻底搞明白备忘录模式

摩天轮转呀转 45

前言:

如今兄弟们对“文本编辑器的三种模式”可能比较关注,我们都需要学习一些“文本编辑器的三种模式”的相关文章。那么小编同时在网络上汇集了一些关于“文本编辑器的三种模式””的相关知识,希望你们能喜欢,朋友们一起来学习一下吧!

本篇讲解Java设计模式中的备忘录模式,分为定义、模式应用前案例、结构、模式应用后案例、适用场景、模式可能存在的困惑和本质探讨7个部分。

定义

备忘录模式是在不破坏封装性的前提下,捕获一个对象的内部状态,并在该对象之外保存这个状态,这样以后就可以将该对象恢复到保存的状态。

在新的分类方式中,备忘录模式被划分至类属性相关需求类别中,其应对的是类的状态属性需要恢复的要求。

模式应用前案例

文本编辑器是一个备忘录模式的典型应用场景。接下来,先来看一下未使用备忘录模式之前的代码实现。

public class TextEditor {//编辑器类-直接实现保存和恢复操作private String content;private String previousContent;public void write(String text) {if(this.content == null ) {this.content = "";        }this.content += text;    }// 保存当前内容为上一个版本的状态public void save() {this.previousContent = this.content;    }// 恢复到上一个版本的状态public void undo(){if(this.previousContent != null){this.content = this.previousContent;        }    }// 获取内容public String getContent(){return this.content;    }}public class Client {//调用方代码public static void main(String[] ars){        TextEditor editor=new TextEditor();        editor.write("Hello, ");        System.out.println(editor.getContent());        editor.save();        editor.write("World!");        System.out.println(editor.getContent());        editor.undo();        System.out.println(editor.getContent());    }}

在上述代码中,主要问题出现在TextEditor类中。为了实现恢复到上一步这个操作,在类中增加了previousContent属性。

如果这个功能是后来才需要增加的,则违背了OCP开闭原则。此外,如果后续要增加恢复上两步的操作,是否还要新增一个doublepreviousContent属性。显然,对于这种类状态(或属性)有变化且能够恢复的场景,应该有更好的解决方案。

结构

备忘录模式的示例实现代码如下。

public class Originator {private String state;public Memento createMemento() {return new Memento(state);    }public void setMemento(Memento memento) {this.state = ((Memento) memento).getState();    }public String getState() {return state;    }public void setState(String state) {this.state = state;    }}public class Memento{private final String state;public Memento(String state) {this.state = state;    }public String getState() {return state;    }}public class Caretaker {private Memento memento;public void setMemento(Memento memento) {this.memento = memento;    }public Memento getMemento() {return memento;    }}public class Client {public static void main(String[] args) {// 创建Originator对象        Originator originator = new Originator();// 设置初始状态        originator.setState("State 1");        System.out.println("Initial State: " + originator.getState());// 创建Caretaker对象并保存备忘录        Caretaker caretaker = new Caretaker();        caretaker.setMemento(originator.createMemento());// 改变Originator的状态        originator.setState("State 2");        System.out.println("State after change: " + originator.getState());// 恢复到之前保存的状态        originator.setMemento(caretaker.getMemento());        System.out.println("State after restore: " + originator.getState());    }}

从备忘录模式的结构和示例代码中,可以看到原有类Originator仅保留了与自身核心业务功能相关的属性,并将其需要恢复状态的属性state放在一个Memento类中保存。

Originator增加了两个比较简洁的方法,一个是创建Memento,一个是从Memento中恢复,所以setMemento方法使用restoreFromMemento会更加准确。

同时,增加了一个Caretaker类,它用于保存、恢复Memento。是恢复到上一个状态还是上两个状态都由Caretaker类专门负责。

不难发现,在备忘录模式下,各个类职责分工明确,核心类Originator专注于核心业务功能,Memento和Caretaker两个支撑类则用于实现状态的保存和恢复。

模式应用后案例

上面文本编辑器的案例,在应用备忘录模式之后的代码实现如下。

TextEditor类删掉了PreviousContent属性,职责更加单一。

public class TextEditor {// 编辑器类(Originator)- 负责创建备忘录和恢复到之前状态private String content;public void write(String text) {if(this.content == null) {this.content = "";        }this.content += text;    }// 创建当前内容对应的备份public EditorMemento createMemento(){return new EditorMemento(this.content);    }// 从传入Mememtor对象中获取内容并进行还原public void restoreFromMemento(EditorMemento memento){this.content = memento.getContent();    }public String getContent() {return this.content;    }}

增加EditorMemento和UndoManager两个类,分别实现TextEditor中Content属性的保存,以及EditorMemento的管理。

public class EditorMemento {// 备忘录类(Memento)- 存储文本编辑器的状态private final String content;public EditorMemento(String content) {this.content = content;    }public String getContent() {return this.content;    }}public class UndoManager {// 管理者类(Caretaker)-负责管理保存和恢复操作    Stack<EditorMemento> emStack =new Stack<>();public void save(EditorMemento memento){this.emStack.push(memento);    }public EditorMemento undo(){if(!this.emStack.empty()){return this.emStack.pop();        }return null;    }}

最后,调用方代码如下。

public class Client {//调用方代码public static void main(String[] ars){        TextEditor editor = new TextEditor();        UndoManager undoManager=new UndoManager();        editor.write("Hello, ");        undoManager.save(editor.createMemento());        editor.write("World!");//undoManager.save(editor.createMemento());        System.out.println(editor.getContent());        editor.restoreFromMemento(undoManager.undo());        System.out.println(editor.getContent());    }}
适用场景

备忘录模式适用的场景非常明确,就是原有类在生命周期变化过程中,其属性的状态还可能需要恢复的场景。

模式可能存在的困惑

困惑1:为什么要有Caretaker类,为什么不能在Memento或Originator中实现保存和恢复功能,这样程序更加简洁?

如果在Originator中实现,又违背了SRP单一职责和OCP开闭原则;如果在Memento实现,这个类功能会变多,每次在Originator中创建Memento对象会占用更多内存,从这个角度就不合适。

困惑2:Memento类只是一个数据的封装类,为什么Originator的状态属性不能直接放在Caretaker中通过一个数据属性来实现?

实际上,许多人在考虑状态恢复的策略时,通常会优先想到这个方案。为了更好地进行说明,这里将代码实现罗列出来。

public class TextEditor {// 编辑器类(Originator)- 负责创建备忘录和恢复到之前状态private String content;public void write(String text) {if(this.content == null) {this.content = "";        }this.content += text;    }// 创建当前内容对应的备份public void saveContent(){        UndoManager.save(this.content);    }// 获取内容并进行还原public void restoreFromContent(){this.content = UndoManager.undo();    }public String getContent() {return this.content;    }}public class UndoManager {// 管理者类(Caretaker)-负责管理保存和恢复操作private static final Stack<String> emStack =new Stack<>();public static void save(String content){        emStack.push(content);    }public static String undo(){if(!emStack.empty()){return emStack.pop();        }return null;    }}public class Client {//调用方代码public static void main(String[] ars){        TextEditor editor = new TextEditor();        editor.write("Hello, ");        editor.saveContent();        editor.write("World!");        System.out.println(editor.getContent());        editor.restoreFromContent();        System.out.println(editor.getContent());    }}

这种方式下,似乎实现起来更加简洁清晰。然而,缺点也比较明显。TextEditor与UndoManager紧耦合的情况下,如果TextEditor要求也能够实现恢复到前两个状态,此时UndoManager增加了一个undo2的方法,那么TextEditor也需要一并修改。

但是在备忘录模式下,TextEditor相当于至于纯数据类Memento进行交互,面对上面的需求并不需要修改,只需要将上两个的Memento传参即可。

困惑3:在关于备忘录模式的一些材料中,会看到宽接口和窄接口,具体是什么含义?

宽接口指的是Memento备忘录对象提供给Originator访问其内部状态的全部信息,包括私有数据。因为Memento里的数据其实就是Originator中要保存、恢复状态的数据,因此Originator需要能访问到具体的数据信息才可以。

窄接口指的是Memento备忘录对象对Caretaker对象指提供必要的信息进行访问和恢复操作。因为Caretaker对象需要是是Memento对象自身,并不需要访问Memento中的数据,因此称之为窄接口。

困惑4:备忘录模式实现之后,对于调用方的交互似乎变得更加复杂?

一件事情往往有得必有失,很难做到两全其美。为了使得Originator不违背SRP单一职责和OCP开闭原则,Client只能增加交互。

如果在Client和备忘录模式的类之间增加一个中间代理类,这样可以减少与调用方之间的交互,但是代价是又新增一个支撑类。

本质

面向对象程序中,一个类在生命周期过程中,其属性构成的状态是会不断变化的。这种变化会带来很多不确定性,尤其在多线程场景下,可能也会引发一些意想不到的问题。因此,Java语言中经常提倡要利用不变性、局部变量等应对这种不确定性。

然而,在某些现实场景下,类随着时间不断变化是有必要的,并且要求还能沿着时间向后回退。此时,备忘录提供了一种管理对象状态的机制,并且让原有对象维持良好的封装性。

标签: #文本编辑器的三种模式