备忘录,备份曾经发生过的历史记录,以防忘记,之后便可以轻松回溯过往。想必我们曾经都干过很多蠢事导致糟糕的结果,当后悔莫及的时候已经是覆水难收了,只可惜这世界上没有后悔药,事后我们能做的只能去弥补过失,总结经验。除非穿越时空,时光倒流,利用爱因斯坦狭义相对论,超越光速回到过去,破镜重圆。
然而世界是残酷的,人类至今最快的载人交通工具连达到光速的万分之一都显得遥不可及,更别说超越了。光速,宇宙间永远无法打破的时空屏障,它像是上帝定义的常量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是在内存中的哦,所以我们一定要依场景灵活运用,切不可生搬硬套。