24.1 如此追女孩子,你还不乐
大家有没有看过尼古拉斯·凯奇主演的《Next》(中文译名为《预见未来》)?尼古拉斯 ·凯奇饰演一个可以预视并且扭转未来的人,其中有一个情节很是让人心动——男女主角见面的那段情节:Cris Johnson(尼古拉斯·凯奇饰演)坐在咖啡吧台前,看着离自己近在咫尺的Callie Ferris(朱莉安·摩尔饰演),计划着怎么认识这个命中注定的女人,看Cris Johnson 如何利用自己的特异功能:
- Cris Johnson端着一杯咖啡走过去,说“你好,可以认识你吗?”被拒绝,恢复到坐在咖 啡吧台前的状态。
- 走过去询问是否可以搭车,被拒绝,恢复原状。
- 帮助解决困境,被拒绝,恢复原状。
- 采用嬉皮士的方式解决困境,被拒绝,恢复原状。
- 帮助解决困境,被打伤,装可怜,Callie Ferris怜惜,于是乎相识了。
看看这是一件多么幸福的事情,追求一个女生可以多次反复地实验,直到找到好的方法和途径为止,这估计是大多数男生都希望获得的特异功能。想想看,看到一个心仪的女生, 我们若反复尝试,总会有一个方法打动她的,多美好的一件事。现在我们还得回到现实生活,我们来分析一下类似事情的经过:
- 复制一个当前状态,保留下来,这个状态就是等会儿搭讪女孩子失败后要恢复的状 态,你不恢复原始状态,这不就露馅儿了吗?
- 每次试探性尝试失败后,都必须恢复到这个原始状态。
- N次试探总有一次成功吧,成功以后即可走成功路线。
想想看,我们这里的场景中最重要的是哪一块?对的,是原始状态的保留和恢复这块, 如何保留一个原始,如何恢复一个原始状态才是最重要的,那想想看,我们应该怎么实现呢?很简单呀,我们可以定义一个中间变量,保留这个原始状态。我们先看看类图,如图24-1所示。
图24-1 男孩状态类图
太简单的类图了,我们来解释一下图中的状态state是什么意思,在某一时间点的所有位置信息、心理信息、环境信息都属于状态,我们这里用了一个标识性的名词state代表所有状态,比如在追女孩子前心情是期待、心理是焦躁不安等。每一次去认识女孩子都是会发生状态变化的,我们使用changeState方法来代替,由于程序比较简单,就没有编写接口,我们来看实现,如代码清单24-1所示。
代码清单24-1 男孩状态类
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| public class Boy { private String state = ""; public void changeState(){ this.state = "心情可能很不好"; } public String getState() { return state; } public void setState(String state) { this.state = state; } }
|
程序是很简单,主要的业务逻辑是在场景类中,我们来看场景类是如何进行状态的保留、恢复的,如代码清单24-2所示。
代码清单24-2 场景类
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| public class Client { public static void main(String[] args) { Boy boy = new Boy(); boy.setState("心情很棒!"); System.out.println("=====男孩现在的状态======"); System.out.println(boy.getState()); Boy backup = new Boy(); backup.setState(boy.getState()); boy.changeState(); System.out.println("\n=====男孩追女孩子后的状态======"); System.out.println(boy.getState()); boy.setState(backup.getState()); System.out.println("\n=====男孩恢复后的状态======"); System.out.println(boy.getState()); } }
|
程序运行结果如下所示:
1 2 3 4 5 6
| =====男孩现在的状态====== 心情很棒! =====男孩追女孩子后的状态====== 心情可能很不好 =====男孩恢复后的状态====== 心情很棒!
|
程序运行正确,输出结果也是我们期望的,但是结果正确并不表示程序是最优的,我们来看看场景类Client,它代表的是高层模块,或者说是非“近亲”模块的调用者,注意看backup 变量的使用,它对于高层模块完全是多余的,为什么一个状态的保存和恢复要让高层模块来负责呢?这应该是Boy类的职责,而不应该让高层模块来完成,也就是破坏了Boy类的封装,或者说Boy类没有封装好,它应该是把backup的定义容纳进来,而不应该让高层模块来定义。
问题我们已经知道了,就是Boy类封装不够,那我们应该如何修改呢?如果在Boy类中再增加一个方法或者其他的内部类来保存这个状态,则对单一职责原则是一种破坏,想想看单一职责原则是怎么说的?一个类的职责应该是单一的,Boy类本身的职责是追求女孩子, 而保留和恢复原始状态则应该由另外一个类来承担,那我们把这个类取名就叫做备忘录,这和大家经常在桌面上贴的那个便签是一个概念,分析到这里我们的思路已经非常清楚了,我们来修改一下类图,如图24-2所示。
图24-2 完善后的男孩状态类图
改动很小,增加了一个新的类Memento,负责状态的保存和备份;同时,在Boy类中增加了创建一份备忘录createMemento和恢复一个备忘录resotreMemento,我们先来看Boy类的变化,如代码清单24-3所示。
代码清单24-3 改进后的男孩状态类
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| public class Boy { private String state = ""; public void changeState(){ this.state = "心情可能很不好"; } public String getState() { return state; } public void setState(String state) { this.state = state; } public Memento createMemento(){ return new Memento(this.state); } public void restoreMemento(Memento _memento){ this.setState(_memento.getState()); } }
|
注意看,确实只增加了两个方法创建备份和恢复备份,至于在什么时候创建备份和恢复备份则是由高层模块决定的。我们再来看备忘录模块,如代码清单24-4所示。
代码清单24-4 备忘录
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| public class Memento { private String state = ""; public Memento(String _state){ this.state = _state; } public String getState() { return state; } public void setState(String state) { this.state = state; } }
|
这就是一个简单的JavaBean,保留男孩当时的状态信息。我们再来看场景类,稍做修改,如代码清单24-5所示。
代码清单24-5 改进后的场景类
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| public class Client { public static void main(String[] args) { Boy boy = new Boy(); boy.setState("心情很棒!"); System.out.println("=====男孩现在的状态======"); System.out.println(boy.getState()); Memento mem = boy.createMemento(); boy.changeState(); System.out.println("\n=====男孩追女孩子后的状态======"); System.out.println(boy.getState()); boy.restoreMemento(mem); System.out.println("\n=====男孩恢复后的状态======"); System.out.println(boy.getState()); } }
|
运行结果保持相同,虽然程序中不再重复定义Boy类的对象了,但是我们还是要关心备忘录,这对迪米特法则是一个亵渎,它告诉我们只和朋友类通信,那这个备忘录对象是我们必须要通信的朋友类吗?对高层模块来说,它最希望要做的就是创建一个备份点,然后在需要的时候再恢复到这个备份点就成了,它不用关心到底有没有备忘录这个类。那根据这一指导思想,我们就需要把备忘录类再包装一下,怎么包装呢?建立一个管理类,就是管理这个备忘录,如图24-3所示。
图24-3 完整的男孩追女生类图
又增加了一个JavaBean,Boy类和Memento没有任何改变,不再赘述。我们来看增加的备忘录管理类,如代码清单24-6所示。
代码清单24-6 备忘录管理者
1 2 3 4 5 6 7 8 9 10
| public class Caretaker { private Memento memento; public Memento getMemento() { return memento; } public void setMemento(Memento memento) { this.memento = memento; } }
|
这个太简单了,非常纯粹的一个JavaBean,甭管它多简单,只要有用就成,我们来看场景类如何调用,如代码清单24-7所示。
代码清单24-7 进一步改进后的场景类
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| public class Client { public static void main(String[] args) { Boy boy = new Boy(); Caretaker caretaker = new Caretaker(); boy.setState("心情很棒!"); System.out.println("=====男孩现在的状态======"); System.out.println(boy.getState()); caretaker.setMemento(boy.createMemento()); boy.changeState(); System.out.println("\n=====男孩追女孩子后的状态======"); System.out.println(boy.getState()); boy.restoreMemento(caretaker.getMemento()); System.out.println("\n=====男孩恢复后的状态======"); System.out.println(boy.getState()); } }
|
注意看黑体部分,就修改了这么多,看看程序的逻辑是不是清晰了很多,需要备份的时候就创建一个备份,然后丢给备忘录管理者进行管理,要取的时候再从管理者手中拿到这个备份。这个备份者就类似于一个备份的仓库管理员,创建一个丢进去,需要的时候再拿出来。这就是备忘录模式。