備忘錄,備份曾經發生過的歷史記錄,以防忘記,之後便可以輕鬆回溯過往。想必我們曾經都乾過很多蠢事導致糟糕的結果,當後悔莫及的時候已經是覆水難收了,只可惜這世界上沒有後悔藥,事後我們能做的只能去彌補過失,總結經驗。除非穿越時空,時光倒流,利用愛因斯坦狹義相對論,超越光速回到過去,破鏡重圓。 然而世界是殘 ...
備忘錄,備份曾經發生過的歷史記錄,以防忘記,之後便可以輕鬆回溯過往。想必我們曾經都乾過很多蠢事導致糟糕的結果,當後悔莫及的時候已經是覆水難收了,只可惜這世界上沒有後悔藥,事後我們能做的只能去彌補過失,總結經驗。除非穿越時空,時光倒流,利用愛因斯坦狹義相對論,超越光速回到過去,破鏡重圓。
然而世界是殘酷的,人類至今最快的載人交通工具連達到光速的萬分之一都顯得遙不可及,更別說超越了。光速,宇宙間永遠無法打破的時空屏障,它像是上帝定義的常量C,將時間牢牢地套死在坐標軸上,自創世宇宙大爆炸開始就讓它不斷流逝,如同播放一部不可回退的電影一樣,暮去朝來,誰也無法打破。
但在電腦世界里,人類便是神一般的存在,各種回滾,倒退,載入歷史顯得稀鬆平常,例如資料庫恢復、游戲存檔載入、操作系統快照恢復、打開備份文檔、手機恢復出廠設置……為了保證極簡風格,我們這裡以文檔操作來舉例說明這個設計模式。
假設某位作者要寫一部科幻小說,當他打開編輯器軟體以及創建文檔開始創作的時候,我們來思考下這個場景需要哪些類。很簡單,首先我們得有一個文檔類Doc。
public class Doc { private String title;//文章標題 private String body;//文章內容 public Doc(String title){//新建文檔先命名 this.title = title; this.body = ""; } public void setTitle(String title) { this.title = title; } public String getTitle() { return title; } public String getBody() { return body; } public void setBody(String body) { this.body = body; } }
沒什麼好說的,一個簡單的Java Bean,包括標題與內容。有了文檔那一定要有編輯器去修改它了,看代碼。
public class Editor {//編輯器 private Doc doc;//文檔引用 public Editor(Doc doc) { System.out.println("<<<打開文檔" + doc.getTitle()); this.doc = doc; show(); } public void append(String txt) { System.out.println("<<<插入操作"); doc.setBody(doc.getBody() + txt); show(); } public void save(){ System.out.println("<<<存檔操作"); } public void delete(){ System.out.println("<<<刪除操作"); doc.setBody(""); show(); } private void show(){//顯示當前文本內容 System.out.println(doc.getBody()); System.out.println("文章結束>>>n"); } }
當編輯器打開一個文檔後會持有其引用,這裡我們寫在編輯器構造方法里。編輯器主要的功能當然是對文檔進行更改了,依然保持簡單的操作模擬,我們只加入append插入功能、delete清空功能,以及save存檔方法和最後的show方法用於顯示文檔內容。一切就緒,接下來看看我們的作者怎樣寫出一部驚世駭俗的科幻小說《AI的覺醒》。
public class Author { public static void main(String[] args) { Editor editor = new Editor(new Doc("《AI的覺醒》")); /* <<<打開文檔《AI的覺醒》 文章結束>>> */ editor.append("第一章 混沌初開"); /* <<<插入操作 第一章 混沌初開 文章結束>>> */ editor.append("n 正文2000字……"); /* <<<插入操作 第一章 混沌初開 正文2000字…… 文章結束>>> */ editor.append("n第二章 荒漠之花n 正文3000字……"); /* <<<插入操作 第一章 混沌初開 正文2000字…… 第二章 荒漠之花 正文3000字…… 文章結束>>> */ editor.delete(); /* <<<刪除操作 文章結束>>> */ } }
鬼才作者開始了創作,一切進行地非常順利,一氣呵成寫完了二章內容(第22行操作),於是他離開電腦去倒了杯咖啡,噩耗在此間發生了,他的熊孩子不知怎麼就按下了Ctr+A,Delete觸發了第31行的操作,導致全文丟失,從記憶體里被清空,而且離開前作者疏忽大意也沒有進行存檔操作,這下徹底完了,5000字的心血付諸東流。
此場景該如何是好?大家都想到了Ctr+z的操作吧?它可以瞬間撤銷上一步操作並回退到前一個版本,不但讓我們有吃後悔藥的機會,而且還不需要頻繁的去存檔備份。那麼這個機制是怎樣實現的呢?既然可以回溯歷史,那一定得有一個歷史備忘類來記錄每步操作後的文本狀態記錄了,它同樣是一個簡單的Java Bean。
public class History { private String body;//用於備忘文章內容 public History(String body){ this.body = body; } public String getBody() { return body; } }
有了這個類,我們便可以記錄文檔的內容快照了,在初始化時把文檔內容傳進來。那誰來生成這些歷史記錄呢?我們可以放在文檔類里,讓文檔類具備創建與恢復歷史記錄的功能,我們對Doc文檔類做如下修改。
public class Doc { private String title;//文章名字 private String body;//文章內容 public Doc(String title){//新建文檔先命名 this.title = title; this.body = ""; } public void setTitle(String title) { this.title = title; } public String getTitle() { return title; } public String getBody() { return body; } public void setBody(String body) { this.body = body; } public History createHistory() { return new History(body);//創建歷史記錄 } public void restoreHistory(History history){ this.body = history.getBody();//恢復歷史記錄 } }
可以看到自第26行開始我們加入了這兩個功能,只要簡單的調用,便可以生成當下的歷史記錄,以及來去自如的恢復內容到任一歷史時刻。接下來得有對歷史記錄的邏輯控制,也就是我們期待已久的撤銷功能了,繼續對編輯器類做如下修改
public class Editor { private Doc doc; private List<History> historyRecords;// 歷史記錄列表 private int historyPosition = -1;// 歷史記錄當前位置 public Editor(Doc doc) { System.out.println("<<<打開文檔" + doc.getTitle()); this.doc = doc; // 註入文檔 historyRecords = new ArrayList<>();// 初始化歷史記錄 backup();// 保存一份歷史記錄 show();//顯示內容 } public void append(String txt) { System.out.println("<<<插入操作"); doc.setBody(doc.getBody() + txt); backup();//操作完成後保存歷史記錄 show(); } public void save(){ System.out.println("<<<存檔操作"); } public void delete(){ System.out.println("<<<刪除操作"); doc.setBody(""); backup();//操作完成後保存歷史記錄 show(); } private void backup() { historyRecords.add(doc.createHistory()); historyPosition++; } private void show() {// 顯示當前文本內容 System.out.println(doc.getBody()); System.out.println("文章結束>>>n"); } public void undo() {// 撤銷操作:如按下Ctr+Z,回到過去。 System.out.println(">>>撤銷操作"); if (historyPosition == 0) { return;//到頭了,不能再撤銷了。 } historyPosition--;//歷史記錄位置回滾一筆 History history = historyRecords.get(historyPosition); doc.restoreHistory(history);//取出歷史記錄並恢復至文檔 show(); } // public void redo(); 省略實現代碼 }
在第3行我們加入了一個歷史記錄列表,它就像是時間軸一樣按順序地按index記錄每個時間點的歷史事件,從某種意義上看它更像是一本歷史書。接下來加入的第32行backup方法會從文檔中拿出快照並插入歷史書,並於每個暴露給客戶端作者的操作方法內被調用,做好歷史的傳承。最後我們加入第42行的撤銷操作,讓時間點回溯一個單位並恢復此處的快照至文檔。當編輯器擁有了撤銷功能後,我們的鬼才作者將高枕無憂的去倒咖啡了。
public class Author { public static void main(String[] args) { Editor editor = new Editor(new Doc("《AI的覺醒》")); /* <<<打開文檔《AI的覺醒》 文章結束>>> */ editor.append("第一章 混沌初開"); /* <<<插入操作 第一章 混沌初開 文章結束>>> */ editor.append("n 正文2000字……"); /* <<<插入操作 第一章 混沌初開 正文2000字…… 文章結束>>> */ editor.append("n第二章 荒漠之花n 正文3000字……"); /* <<<插入操作 第一章 混沌初開 正文2000字…… 第二章 荒漠之花 正文3000字…… 文章結束>>> */ editor.delete(); /* <<<刪除操作 文章結束>>> */ //吃下後悔藥,我的世界又完整了。 editor.undo(); /* >>>撤銷操作 第一章 混沌初開 正文2000字…… 第二章 荒漠之花 正文3000字…… 文章結束>>> */ } }
可以看到,熊孩子做了delete操作後,作者輕鬆淡定地按下了Ctr+z,一切恢復如初,世界依舊美好,輓回那逝去的青蔥歲月。當然,代碼中我們略去了一些功能,比如讀者還可以加入重做redo操作,彈指之間,讓歷史在時間軸上來去自如,我的電腦我做主,時空穿梭,逆天之做。
誠然,任何模式都有其優缺點,備忘錄雖然看起來完美,但如果歷史狀態內容過大,會導致記憶體消耗嚴重,別忘了那邊歷史書的list是在記憶體中的哦,所以我們一定要依場景靈活運用,切不可生搬硬套。