模板方法模式
周三,9:00,我刚刚坐到位置,打开电脑准备开始干活。
“小三,小三,叫一下其它同事,到会议室,开会”老大跑过来吼,带着淫笑。还不等大家坐稳,老大就开讲了,
“告诉大家一个好消息,昨天终于把牛叉模型公司的口子打开了,要我们做悍马模型,虽然是第一个车辆模型,但是我们有能力,有信心做好,我们一定要…(中间省略 20 分钟的讲话,如果你听过领导人的讲话,这个你应该能够续上)”
动员工作做完了,那就开始压任务了,“这次时间是非常紧张的,只有一个星期的时间,小三,你负责在一个星期的时间把这批 10 万车模(注:车模是车辆模型的意思,不是香车美女那个车模)建设完成…”
“一个星期?这个…,是真做不完,要做分析,做模板,做测试,还要考虑扩展性、稳定性、健壮性等,时间实在是太少了”还没等老大说完,我就急了,再不急我的小命就折在上面了!
“那这样,你只做实现,不考虑使用设计模式,扩展性等都不用考虑”老大又把我压回去了。
“不考虑设计模式?那…”
哎,领导已经布置任务了,那就开始死命的做吧,命苦不能怨政府,点背不能怪社会呀,然后就开始准备动手做,在做之前先介绍一下我们公司的背景,我们公司是做模型生产的,做过桥梁模型、建筑模型、机械模型,甚至是一些政府、军事的机密模型,这个不能说,就是把真实的实物按照一定的比例缩小或放大,用于试验、分析、量化或者是销售等等,上面提到的牛叉模型公司专门销售车辆模型的公司,自己不生产,我们公司是第一次从牛叉模型公司接单,那我怎么着也要把活干好,可时间很紧张呀,怎么办?
既然领导都说了,不考虑扩展性,那好办,我先设计个类图:
非常简单的实现,你要悍马模型,我就给你悍马模型,先写个抽象类,然后两个不同型号的模型实现类,那我们把这个程序实现出来:
HummerModel 抽象类的程序清单如下:package com.qssqc.formwork; /** * Hummer Model是悍马车辆模型的意思,不是悍马美女车模 * * @author 清水三千尺 * */ public abstract class HummerModel { /* * 首先,这个模型要能够被发动起来,别管是手摇发动,还是电力发动, * 反正是要能够发动起来,那这个实现要在实现类里了 */ public abstract void start(); // 能发动,那还要能停下来,那才是真本事 public abstract void stop(); // 喇叭会出声音,是滴滴叫,还是哔哔叫 public abstract void alarm(); // 引擎会轰隆隆的响,不响那是假的 public abstract void engineBoom(); // 那模型应该会跑吧,别管是人推的,还是电力驱动,总之要会跑 public abstract void run(); }
H1 型号悍马的定义如下:
package com.qssqc.formwork; /** * 悍马车是每个越野者的爱,其中H1接近军用系列 * * @author 清水三千尺 * */ public class HummerH1Model extends HummerModel { @Override public void alarm() { System.out.println("悍马H1鸣笛..."); } @Override public void engineBoom() { System.out.println("悍马H1引擎声音是这样在..."); } @Override public void start() { System.out.println("悍马H1发动..."); } @Override public void stop() { System.out.println("悍马H1停车..."); } /* * 这个方法是很有意思的,它要跑,那肯定要启动,停止了等, * 也就是要调其他方法 */ @Override public void run() { // 先发动汽车 this.start(); // 引擎开始轰鸣 this.engineBoom(); // 然后就开始跑了,跑的过程中遇到一条狗挡路,就按喇叭 this.alarm(); // 到达目的地就停车 this.stop(); } }
然后看悍马 H2 型号的实现:
package com.qssqc.formwork; /** * H1和H2有什么差别,还真不知道,真没接触过悍马 * * @author 清水三千尺 * */ public class HummerH2Model extends HummerModel { @Override public void alarm() { System.out.println("悍马H2鸣笛..."); } @Override public void engineBoom() { System.out.println("悍马H2引擎声音是这样在..."); } @Override public void start() { System.out.println("悍马H2发动..."); } @Override public void stop() { System.out.println("悍马H1停车..."); } /* * H2要跑,那肯定要启动,停止了等,也就是要调其他方法 */ @Override public void run() { // 先发动汽车 this.start(); // 引擎开始轰鸣 this.engineBoom(); // 然后就开始跑了,跑的过程中遇到一条狗挡路,就按喇叭 this.alarm(); // 到达目的地就停车 this.stop(); } }
然后程序写到这里,你就看到问题了,run 方法的实现应该在抽象类上,不应该在实现类上,好,我们修改一下类图和实现:
就把 run 方法放到了抽象类中,那代码也相应的改变一下,先看HummerModel.java:
修改代码package com.qssqc.formwork; /** * Hummer Model是悍马车辆模型的意思,不是悍马美女车模 * * @author 清水三千尺 * */ public abstract class HummerModel { /* * 首先,这个模型要能够被发动起来,别管是手摇发动,还是电力发动, * 反正是要能够发动起来,那这个实现要在实现类里了 */ public abstract void start(); // 能发动,那还要能停下来,那才是真本事 public abstract void stop(); // 喇叭会出声音,是滴滴叫,还是哔哔叫 public abstract void alarm(); // 引擎会轰隆隆的响,不响那是假的 public abstract void engineBoom(); // 那模型应该会跑吧,别管是人推的,还是电力驱动,总之要会跑 public void run() { //先发动汽车 this.start(); //引擎开始轰鸣 this.engineBoom(); //然后就开始跑了,跑的过程中遇到一条狗挡路,就按喇叭 this.alarm(); //到达目的地就停车 this.stop(); } }
下面是 HummerH1Model.java 程序清单:
package com.qssqc.formwork; /** * 悍马车是每个越野者的爱,其中H1接近军用系列 * * @author 清水三千尺 * */ public class HummerH1Model extends HummerModel { @Override public void alarm() { System.out.println("悍马H1鸣笛..."); } @Override public void engineBoom() { System.out.println("悍马H1引擎声音是这样在..."); } @Override public void start() { System.out.println("悍马H1发动..."); } @Override public void stop() { System.out.println("悍马H1停车..."); } }
下面是 HummerH2Model.java 的程序清单:
package com.qssqc.formwork; /** * H1和H2有什么差别,还真不知道,真没接触过悍马 * * @author 清水三千尺 * */ public class HummerH2Model extends HummerModel { @Override public void alarm() { System.out.println("悍马H2鸣笛..."); } @Override public void engineBoom() { System.out.println("悍马H2引擎声音是这样在..."); } @Override public void start() { System.out.println("悍马H2发动..."); } @Override public void stop() { System.out.println("悍马H1停车..."); } }
类图修改完毕了,程序也该好了,提交给老大,老大一看,挺好,就开始生产了,并提交给客户使用了,那客户是如何使用的呢?类图上增加一个 Client 类,就是客户,我们这个是用 main 函数来代替他使用,类图如下:
然后看增加的 Client.java 程序,非常的简单:
package com.qssqc.formwork; /** * 客户开始使用这个模型 * * @author 清水三千尺 * */ public class Client { public static void main(String[] args) { // 客户开着H1型号,出去遛弯了 HummerModel h1 = new HummerH1Model(); h1.run(); // 汽车跑起来了; // 客户开H2型号,出去玩耍了 HummerModel h2 = new HummerH2Model(); h2.run(); } }
运行的结果如下:
非常非常的简单,那如果我告诉这就是模板方法模式你会不会很不屑呢?就这模式,太简单了,我一直在使用呀,是的,你经常在使用,但你不知道这是模板方法模式,那些所谓的高手就可以很牛 X 的说“用模板方法模式就可以实现…”,你还要很崇拜的看着,哇,牛人,模板方法模式是什么呀?
然后我们继续回顾我们这个模型,回头一想,不对呀,需求分析的有点问题,客户要关心模型的启动,停止,鸣笛,引擎声音吗?他只要在 run 的过程中,听到或看都成了呀,暴露那么多的方法干啥?好了,我们重新修改一下类图:
把抽象类上的四个方法设置为 protected 访问权限,好了,既然客户不关心这几个方法,而且这四个方法都是由子类来实现的,那就设置成 protected 模式。咦~,那还有个缺陷,run 方法既然子类都不修改,那是不是可以设置成 final 类型呢?是滴是滴,类图如下:
好了,这才是模板方法模式,就是这个样子,我们只要修改抽象类代码就可以了,HummerModel.java程序清单如下:
package com.qssqc.formwork; /** * Hummer Model是悍马车辆模型的意思,不是悍马美女车模 * * @author 清水三千尺 * */ public abstract class HummerModel { /* * 首先,这个模型要能够被发动起来,别管是手摇发动,还是电力发动, * 反正是要能够发动起来,那这个实现要在实现类里了 */ protected abstract void start(); // 能发动,那还要能停下来,那才是真本事 protected abstract void stop(); // 喇叭会出声音,是滴滴叫,还是哔哔叫 protected abstract void alarm(); // 引擎会轰隆隆的响,不响那是假的 protected abstract void engineBoom(); // 那模型应该会跑吧,别管是人推的,还是电力驱动,总之要会跑 final void run() { //先发动汽车 this.start(); //引擎开始轰鸣 this.engineBoom(); //然后就开始跑了,跑的过程中遇到一条狗挡路,就按喇叭 this.alarm(); //到达目的地就停车 this.stop(); } }
其他的子类都不用修改(如果要修改,就是把四个方法的访问权限由 public 修改protected,大家请看这个 run 方法,他定义了调用其他方法的顺序,并且子类是不能修改的,这个叫做模板方法;start、stop、alarm、engineBoom 这四个方法是子类必须实现的,而且这四个方法的修改对应了不同的类,这个叫做基本方法,基本方法又分为三种:在抽象类中实现了的基本方法叫做具体方法;在抽象类中没有实现,在子类中实现了叫做抽象方法,我们这四个基本方法都是抽象方法,由子类来实现的;还有一种叫做钩子方法,这个等会讲。
到目前为止,这两个模型都稳定的运行,突然有一天,老大又找到了我, “客户提出新要求了,那个喇叭想让它响就响,你看你设计的模型,车子一启动,喇叭就狂响,赶快修改一下”,确实是设计缺陷,呵呵,不过是我故意的,那我们怎么修改呢?看修改后的类图:
增加一个方法,isAlarm(),喇嘛要不要响,这就是钩子方法(Hook Method),那我们只要修改一下抽象类就可以了:
钩子方法package com.qssqc.formwork; /** * Hummer Model是悍马车辆模型的意思,不是悍马美女车模 * * @author 清水三千尺 * */ public abstract class HummerModel { /* * 首先,这个模型要能够被发动起来,别管是手摇发动,还是电力发动, 反正是要能够发动起来,那这个实现要在实现类里了 */ protected abstract void start(); // 能发动,那还要能停下来,那才是真本事 protected abstract void stop(); // 喇叭会出声音,是滴滴叫,还是哔哔叫 protected abstract void alarm(); // 引擎会轰隆隆的响,不响那是假的 protected abstract void engineBoom(); // 那模型应该会跑吧,别管是人推的,还是电力驱动,总之要会跑 final void run() { // 先发动汽车 this.start(); // 引擎开始轰鸣 this.engineBoom(); // 喇嘛想让它响就响,不想让它响就不响 if (this.isAlarm()) { this.alarm(); } // 到达目的地就停车 this.stop(); } // 钩子方法,默认喇叭是会响的 protected boolean isAlarm() { return true; } }
钩子方法模式是由抽象类来实现的,子类可以重写的,H2 型号的悍马是不会叫的,喇叭是个摆设,看HummerH2Model.java 代码:
package com.qssqc.formwork; /** * H1和H2有什么差别,还真不知道,真没接触过悍马 * * @author 清水三千尺 * */ public class HummerH2Model extends HummerModel { @Override public void alarm() { System.out.println("悍马H2鸣笛..."); } @Override public void engineBoom() { System.out.println("悍马H2引擎声音是这样在..."); } @Override public void start() { System.out.println("悍马H2发动..."); } @Override public void stop() { System.out.println("悍马H1停车..."); } // 默认没有喇叭的 @Override protected boolean isAlarm() { return false; } }
那 H2 型号的模型都没有喇叭,就是按了喇叭也没有声音,那客户端这边的调用没有任何修改,出来的结果就不同,我们先看 Client.java 程序:
package com.qssqc.formwork; /** * 客户开始使用这个模型 * * @author 清水三千尺 * */ public class Client { public static void main(String[] args) { HummerH2Model h2 = new HummerH2Model(); h2.run(); // H2型号的悍马跑起来 } }
那 H1 又有所不同了,它的喇叭要不要响是由客户来决定,其实在类图上已经标明了 setAlarm 这个方法,我们看 HummerH1Model.java 的代码:
package com.qssqc.formwork; /** * 悍马车是每个越野者的爱,其中H1接近军用系列 * * @author 清水三千尺 * */ public class HummerH1Model extends HummerModel { private boolean alarmFlag = true; // 是否要响喇叭 @Override public void alarm() { System.out.println("悍马H1鸣笛..."); } @Override public void engineBoom() { System.out.println("悍马H1引擎声音是这样在..."); } @Override public void start() { System.out.println("悍马H1发动..."); } @Override public void stop() { System.out.println("悍马H1停车..."); } @Override protected boolean isAlarm() { return this.alarmFlag; } // 要不要响喇叭,是有客户的来决定的 public void setAlarm(boolean isAlarm) { this.alarmFlag = isAlarm; } }
这段代码呢修改了两个地方,一是重写了父类的 isAlarm()方法,一是增加了一个 setAlarm 方法,由调用者去决定是否要这个功能,也就是喇叭要不要滴滴答答的响,哈哈,那我们看看 Client.java 的修改:
package com.qssqc.formwork; /** * 客户开始使用这个模型 * * @author 清水三千尺 * */ public class Client { public static void main(String[] args) { // 客户开着H1型号,出去遛弯了 HummerH1Model h1 = new HummerH1Model(); h1.setAlarm(true); h1.run(); // 汽车跑起来了; } }
运行的结果如下:
看到没,这个模型 run 起来就有声音了,那当然把 h1.setAlarm(false)运行起来喇叭就没有声音了,钩子方法的作用就是这样滴。
那我们总结一下模板方法模式,模板方法模式就是在模板方法中按照一个的规则和顺序调用基本方法,具体到我们上面那个例子就是 run 方法按照规定的顺序(先调用 start,然后再调用 engineBoom,再调用 alarm,后调用 stop)调用本类的其他方法,并且由 isAlarm 方法的返回值确定 run 中的执行顺序变更,通用类图如下:
其中 TemplateMethod 就是模板方法,operation1 和 operation2 就是基本方法,模板方法是通过汇总或排序基本方法而产生的结果集。模板方法在一些开源框架中应用很多,它提供了一个抽象类,然后开源框架写了一堆子类,在《XXX In Action》中就说明了,如果你需要扩展功能,可以继承了这个抽象类,然后修改 protected 方法,再然后就是调用一个类似 execute 方法,就完成你的扩展开发,确实是一种简单的模式。
结束语
代码结构
初 级 程 序 员 在 写 程 序 的 时 候 经 常 会 问 高 手 “ 父 类 怎 么 调 用 子 类 的 方 法 ”, 这 个 问 题 很 有 普 遍 性 , 反 正 我是被问过好几回,那么父类是否可以调用子类的方法呢?我的回答是能,但强烈的、极度的不建议,怎么做呢?
- 把子类传递到父类的有参构造中,然后调用;
- 使用反射的方式调用,你使用了反射还有谁不能调用的?!
- 父类调用子类的静态方法。
这三种都是父类直接调用子类的方法,好用不?好用!解决问题了吗?解决了!项目中允许使用不?不允许!我就一直没有搞懂为什么要父类调用子类的方法,如果一定要调用子类,那为什么要继承它呢?搞不懂。其实这个问题可以换个角度去理解,在重写了父类部分方法后,再调用从父类继承的方法,产生不 同 的 结 果 ( 而 这 正 是 模 板 方 法 模 式 ), 这 是 不 是 也 可 以 理 解 为 父 类 调 用 了 子 类 的 方 法 呢 ? 你 修 改 了 子 类 ,影响了父类的结果,模板方法模式就是这样效果。