一文徹底搞明白備忘錄模式
本篇講解Java設(shè)計模式中的備忘錄模式,分為定義、模式應(yīng)用前案例、結(jié)構(gòu)、模式應(yīng)用后案例、適用場景、模式可能存在的困惑和本質(zhì)探討7個部分。
定義
備忘錄模式是在不破壞封裝性的前提下,捕獲一個對象的內(nèi)部狀態(tài),并在該對象之外保存這個狀態(tài),這樣以后就可以將該對象恢復(fù)到保存的狀態(tài)。
在新的分類方式中,備忘錄模式被劃分至類屬性相關(guān)需求類別中,其應(yīng)對的是類的狀態(tài)屬性需要恢復(fù)的要求。
模式應(yīng)用前案例
文本編輯器是一個備忘錄模式的典型應(yīng)用場景。接下來,先來看一下未使用備忘錄模式之前的代碼實現(xiàn)。
public class TextEditor {//編輯器類-直接實現(xiàn)保存和恢復(fù)操作
private String content;
private String previousContent;
public void write(String text) {
if(this.content == null ) {
this.content = "";
}
this.content += text;
}
// 保存當(dāng)前內(nèi)容為上一個版本的狀態(tài)
public void save() {
this.previousContent = this.content;
}
// 恢復(fù)到上一個版本的狀態(tài)
public void undo(){
if(this.previousContent != null){
this.content = this.previousContent;
}
}
// 獲取內(nèi)容
public String getContent(){
return this.content;
}
}
public class Client {//調(diào)用方代碼
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());
}
}
在上述代碼中,主要問題出現(xiàn)在TextEditor類中。為了實現(xiàn)恢復(fù)到上一步這個操作,在類中增加了previousContent屬性。
如果這個功能是后來才需要增加的,則違背了OCP開閉原則。此外,如果后續(xù)要增加恢復(fù)上兩步的操作,是否還要新增一個doublepreviousContent屬性。顯然,對于這種類狀態(tài)(或?qū)傩裕┯凶兓夷軌蚧謴?fù)的場景,應(yīng)該有更好的解決方案。
結(jié)構(gòu)
備忘錄模式的示例實現(xiàn)代碼如下。
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) {
// 創(chuàng)建Originator對象
Originator originator = new Originator();
// 設(shè)置初始狀態(tài)
originator.setState("State 1");
System.out.println("Initial State: " + originator.getState());
// 創(chuàng)建Caretaker對象并保存?zhèn)渫? Caretaker caretaker = new Caretaker();
caretaker.setMemento(originator.createMemento());
// 改變Originator的狀態(tài)
originator.setState("State 2");
System.out.println("State after change: " + originator.getState());
// 恢復(fù)到之前保存的狀態(tài)
originator.setMemento(caretaker.getMemento());
System.out.println("State after restore: " + originator.getState());
}
}
從備忘錄模式的結(jié)構(gòu)和示例代碼中,可以看到原有類Originator僅保留了與自身核心業(yè)務(wù)功能相關(guān)的屬性,并將其需要恢復(fù)狀態(tài)的屬性state放在一個Memento類中保存。
Originator增加了兩個比較簡潔的方法,一個是創(chuàng)建Memento,一個是從Memento中恢復(fù),所以setMemento方法使用restoreFromMemento會更加準(zhǔn)確。
同時,增加了一個Caretaker類,它用于保存、恢復(fù)Memento。是恢復(fù)到上一個狀態(tài)還是上兩個狀態(tài)都由Caretaker類專門負(fù)責(zé)。
不難發(fā)現(xiàn),在備忘錄模式下,各個類職責(zé)分工明確,核心類Originator專注于核心業(yè)務(wù)功能,Memento和Caretaker兩個支撐類則用于實現(xiàn)狀態(tài)的保存和恢復(fù)。
模式應(yīng)用后案例
上面文本編輯器的案例,在應(yīng)用備忘錄模式之后的代碼實現(xiàn)如下。
TextEditor類刪掉了PreviousContent屬性,職責(zé)更加單一。
public class TextEditor {// 編輯器類(Originator)- 負(fù)責(zé)創(chuàng)建備忘錄和恢復(fù)到之前狀態(tài)
private String content;
public void write(String text) {
if(this.content == null) {
this.content = "";
}
this.content += text;
}
// 創(chuàng)建當(dāng)前內(nèi)容對應(yīng)的備份
public EditorMemento createMemento(){
return new EditorMemento(this.content);
}
// 從傳入Mememtor對象中獲取內(nèi)容并進行還原
public void restoreFromMemento(EditorMemento memento){
this.content = memento.getContent();
}
public String getContent() {
return this.content;
}
}
增加EditorMemento和UndoManager兩個類,分別實現(xiàn)TextEditor中Content屬性的保存,以及EditorMemento的管理。
public class EditorMemento {// 備忘錄類(Memento)- 存儲文本編輯器的狀態(tài)
private final String content;
public EditorMemento(String content) {
this.content = content;
}
public String getContent() {
return this.content;
}
}
public class UndoManager {// 管理者類(Caretaker)-負(fù)責(zé)管理保存和恢復(fù)操作
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;
}
}
最后,調(diào)用方代碼如下。
public class Client {//調(diào)用方代碼
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());
}
}
適用場景
備忘錄模式適用的場景非常明確,就是原有類在生命周期變化過程中,其屬性的狀態(tài)還可能需要恢復(fù)的場景。
模式可能存在的困惑
困惑1:為什么要有Caretaker類,為什么不能在Memento或Originator中實現(xiàn)保存和恢復(fù)功能,這樣程序更加簡潔?
如果在Originator中實現(xiàn),又違背了SRP單一職責(zé)和OCP開閉原則;如果在Memento實現(xiàn),這個類功能會變多,每次在Originator中創(chuàng)建Memento對象會占用更多內(nèi)存,從這個角度就不合適。
困惑2:Memento類只是一個數(shù)據(jù)的封裝類,為什么Originator的狀態(tài)屬性不能直接放在Caretaker中通過一個數(shù)據(jù)屬性來實現(xiàn)?
實際上,許多人在考慮狀態(tài)恢復(fù)的策略時,通常會優(yōu)先想到這個方案。為了更好地進行說明,這里將代碼實現(xiàn)羅列出來。
public class TextEditor {// 編輯器類(Originator)- 負(fù)責(zé)創(chuàng)建備忘錄和恢復(fù)到之前狀態(tài)
private String content;
public void write(String text) {
if(this.content == null) {
this.content = "";
}
this.content += text;
}
// 創(chuàng)建當(dāng)前內(nèi)容對應(yīng)的備份
public void saveContent(){
UndoManager.save(this.content);
}
// 獲取內(nèi)容并進行還原
public void restoreFromContent(){
this.content = UndoManager.undo();
}
public String getContent() {
return this.content;
}
}
public class UndoManager {// 管理者類(Caretaker)-負(fù)責(zé)管理保存和恢復(fù)操作
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 {//調(diào)用方代碼
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());
}
}
這種方式下,似乎實現(xiàn)起來更加簡潔清晰。然而,缺點也比較明顯。TextEditor與UndoManager緊耦合的情況下,如果TextEditor要求也能夠?qū)崿F(xiàn)恢復(fù)到前兩個狀態(tài),此時UndoManager增加了一個undo2的方法,那么TextEditor也需要一并修改。
但是在備忘錄模式下,TextEditor相當(dāng)于至于純數(shù)據(jù)類Memento進行交互,面對上面的需求并不需要修改,只需要將上兩個的Memento傳參即可。
困惑3:在關(guān)于備忘錄模式的一些材料中,會看到寬接口和窄接口,具體是什么含義?
寬接口指的是Memento備忘錄對象提供給Originator訪問其內(nèi)部狀態(tài)的全部信息,包括私有數(shù)據(jù)。因為Memento里的數(shù)據(jù)其實就是Originator中要保存、恢復(fù)狀態(tài)的數(shù)據(jù),因此Originator需要能訪問到具體的數(shù)據(jù)信息才可以。
窄接口指的是Memento備忘錄對象對Caretaker對象指提供必要的信息進行訪問和恢復(fù)操作。因為Caretaker對象需要是是Memento對象自身,并不需要訪問Memento中的數(shù)據(jù),因此稱之為窄接口。
困惑4:備忘錄模式實現(xiàn)之后,對于調(diào)用方的交互似乎變得更加復(fù)雜?
一件事情往往有得必有失,很難做到兩全其美。為了使得Originator不違背SRP單一職責(zé)和OCP開閉原則,Client只能增加交互。
如果在Client和備忘錄模式的類之間增加一個中間代理類,這樣可以減少與調(diào)用方之間的交互,但是代價是又新增一個支撐類。
本質(zhì)
面向?qū)ο蟪绦蛑校粋€類在生命周期過程中,其屬性構(gòu)成的狀態(tài)是會不斷變化的。這種變化會帶來很多不確定性,尤其在多線程場景下,可能也會引發(fā)一些意想不到的問題。因此,Java語言中經(jīng)常提倡要利用不變性、局部變量等應(yīng)對這種不確定性。
然而,在某些現(xiàn)實場景下,類隨著時間不斷變化是有必要的,并且要求還能沿著時間向后回退。此時,備忘錄提供了一種管理對象狀態(tài)的機制,并且讓原有對象維持良好的封裝性。