管中窥豹——模板方法怎么用?
在谈什么是模板方法模式之前,我想我们还是从死板的教条定义中抽离出来,通过一个小例子来看看什么是模板方法模式,以及它能给我们带来怎样的便利。
假如有一家宝马车制造厂商想对一批宝马车进行测试,这里暂且定为要测试的车型为X5和X6。根据这个要求,其实我们可以很容易的得到如下设计:
-
要有一个共同的抽象类BaomaModel,在这个类中定义了宝马车所具备的一些共同特性。
-
BaomaX5和BaomaX6继承了BaomaModel
通过设计我们可以得到如下代码:
//抽象类BaomaModel
public abstract class BaomaModel {
//发动汽车
protected abstract void start();
//停止汽车
protected abstract void stop();
//鸣笛
protected abstract void alarm();
//启动引擎
protected abstract void engineBoom();
//跑起来
protected abstract void run()
}
//子类BaomaX5
public class BaomaX5 extends BaomaModel {
public void setAlarmFlag(boolean alarmFlag) {
this.alarmFlag = alarmFlag;
}
protected void start() {
System.out.println("我是X5,我要发动了。。。");
}
protected void stop() {
System.out.println("我要停止了。。。");
}
protected void alarm() {
System.out.println("我要鸣笛,前面的家伙绕道。。。");
}
protected void engineBoom() {
System.out.println("我要发动引擎了。。。");
}
protected void run() {
//发动汽车
this.start();
//启动引擎
this.engineBoom();
//鸣笛
this.alarm();
//停止汽车
this.stop();
}
}
//子类BaomaX6
public class BaomaX6 extends BaomaModel {
protected void start() {
System.out.println("我是X6,我要发动了。。。");
}
protected void stop() {
System.out.println("我要停止了。。。");
}
protected void alarm() {
System.out.println("我要鸣笛,前面的家伙绕道。。。");
}
protected void engineBoom() {
System.out.println("我要发动引擎了。。。");
}
protected void run() {
//发动汽车
this.start();
//启动引擎
this.engineBoom();
//鸣笛
this.alarm();
//停止汽车
this.stop();
}
}
在场景类中运行一下我们会得到如下的结果:
public class Client {
public static void main(String[] args) {
BaomaModel x5 = new BaomaX5();
BaomaModel x6 = new BaomaX6();
x5.run();
System.out.println("==============================");
x6.run();
}
}
//结果如下:
我是X5,我要发动了。。。
我要发动引擎了。。。
我要鸣笛,前面的家伙绕道。。。
我要停止了。。。
==============================
我是X6,我要发动了。。。
我要发动引擎了。。。
我要鸣笛,前面的家伙绕道。。。
我要停止了。。。
看着运行结果输出了,我们长舒了一口气,终于实现了。可是,难道实现了就完了吗,如果想成为一个有些的程序员,仅仅满足于实现是不够的,我们还应该仔细揣摩自己的设计,看看有没有可以改进的地方。就拿我们刚刚的这个设计来说,仔细揣摩会发现如下问题:
- 对于BaomaX5和BaomaX6来说,run()的功能是一样的,无非就是定义了运行顺序,但是我们却在每一个子类中都重写了这个方法,增加了代码的冗余,这似乎没有必要。想一想,我们可以怎么办?
其实程序界伟大的先哲们(The Gang of Four:Erich Gamma,Richard Helm,Ralph Johnson,John Vlissides)早就发现了这个问题,并为我们总结出了一个解决此类问题的设计模式——模板方法模式,我们先来修改一下这个设计:
//抽象了BaomaModel
public abstract class BaomaModel {
//发动汽车
protected abstract void start();
//停止汽车
protected abstract void stop();
//鸣笛
protected abstract void alarm();
//启动引擎
protected abstract void engineBoom();
//钩子函数(hook):用来控制汽车是否鸣笛
protected boolean isAlarm() {
return true;
}
/**
* 定义一个模板方法,使子类按照定义的顺序执行
*/
protected final void run() {
//发动汽车
this.start();
//启动引擎
this.engineBoom();
//鸣笛
this.alarm();
//停止汽车
this.stop();
}
}
//子类BaomaX5
public class BaomaX5 extends BaomaModel {
public void setAlarmFlag(boolean alarmFlag) {
this.alarmFlag = alarmFlag;
}
protected void start() {
System.out.println("我是X5,我要发动了。。。");
}
protected void stop() {
System.out.println("我要停止了。。。");
}
protected void alarm() {
System.out.println("我要鸣笛,前面的家伙绕道。。。");
}
protected void engineBoom() {
System.out.println("我要发动引擎了。。。");
}
}
//子类BaomaX6
public class BaomaX6 extends BaomaModel {
protected void start() {
System.out.println("我是X6,我要发动了。。。");
}
protected void stop() {
System.out.println("我要停止了。。。");
}
protected void alarm() {
System.out.println("我要鸣笛,前面的家伙绕道。。。");
}
protected void engineBoom() {
System.out.println("我要发动引擎了。。。");
}
}
在场景类运行代码得到结果如下:
public class Client {
public static void main(String[] args) {
BaomaModel x5 = new BaomaX5();
BaomaModel x6 = new BaomaX6();
x5.run();
System.out.println("==============================");
x6.run();
}
}
//结果如下:
我是X5,我要发动了。。。
我要发动引擎了。。。
我要鸣笛,前面的家伙绕道。。。
我要停止了。。。
==============================
我是X6,我要发动了。。。
我要发动引擎了。。。
我要鸣笛,前面的家伙绕道。。。
我要停止了。。。
看到这种设计的优势了吗?其实总结一下可以得到:
-
父类可以定义一个框架,实际上就是执行顺序,然后让子类去执行。子类只管实现自己的业务逻辑方法,而不必要操心执行流程。
-
减少了代码的冗余,增加了可扩展性
探索根源——什么是模板方法模式?
通过上面的例子,我们已经对模板方法模式有了一个初步的了解,现在就详细介绍一下什么是模板方法模式,以及应用情况。
模板方法模式的定义
定义:定义一个操作中的算法框架,而将一些步骤延迟到子类中。使得子类可以玩不改变一个算法的结构即可重新定义该算法的某些特定步骤。下面是模板方法模式的通用类图:
通过类图我们可以看到,模板方法模式使用的是继承机制,AbstractClass定义了一组抽象模板,ConcreteClass继承于AbstractClass并实现抽象方法,这是一种很好理解的关系。其中AbstractClass的方法可以分为两类:
-
基本方法:也被称为基本操作,是子类实现的方法,并且在模板方法中被调用。
-
模板方法:可以有一个或者多个,一般是一个具体方法,定义了一个框架,实现对基本方法的调度,完成固定的逻辑。
注意:因为模板方法是一种定义好的框架,一般是不允许恶意修改的,所以通常会在模板方法上加final关键字,防止被重写。
抽象模板中的基本方法尽量设计为protected类型,符合迪米特法则。不需要暴露的细节尽量设计为私有。实现类如非必要,不要扩大父类中的访问权限
模板方法模式的应用——钩子函数(hook)
基本操作的话就是上面讲的例子,其实模板方法模式还是很简单的。这里的话说一个模板方法模式的扩展内容——钩子函数的使用。下面是钩子函数的类图:
直接通过举例来说明吧。就拿刚刚测试宝马车的例子来说,其实按不按喇叭是一个随机操作,你可以按也可以不按,但是由于我们把它写死在了模板方法中,那么不管你愿不愿意,喇叭都会响,这是一种很不爽的事情,也不利于功能的扩展,所以钩子函数就诞生了,通过钩子函数我们就可以约束模板的行为。下面看一下代码:
//父类BaomaModel
public abstract class BaomaModel {
//发动汽车
protected abstract void start();
//停止汽车
protected abstract void stop();
//鸣笛
protected abstract void alarm();
//启动引擎
protected abstract void engineBoom();
//钩子函数(hook):用来控制汽车是否鸣笛
protected boolean isAlarm() {
return true;
}
/**
* 定义一个模板方法,使子类按照定义的顺序执行
*/
protected void run() {
//发动汽车
this.start();
//启动引擎
this.engineBoom();
//是否鸣笛
if (isAlarm()) {
this.alarm();
}
//停止汽车
this.stop();
}
}
//子类BaomaX5
public class BaomaX5 extends BaomaModel {
private boolean alarmFlag = true; //默认喇叭可以响
public void setAlarmFlag(boolean alarmFlag) {
this.alarmFlag = alarmFlag;
}
protected void start() {
System.out.println("我是X5,我要发动了。。。");
}
protected void stop() {
System.out.println("我要停止了。。。");
}
protected void alarm() {
System.out.println("我要鸣笛,前面的家伙绕道。。。");
}
protected void engineBoom() {
System.out.println("我要发动引擎了。。。");
}
@Override
protected boolean isAlarm() {
return this.alarmFlag;
}
}
//子类BaomaX6
public class BaomaX6 extends BaomaModel {
protected void start() {
System.out.println("我是X6,我要发动了。。。");
}
protected void stop() {
System.out.println("我要停止了。。。");
}
protected void alarm() {
System.out.println("我要鸣笛,前面的家伙绕道。。。");
}
protected void engineBoom() {
System.out.println("我要发动引擎了。。。");
}
@Override
protected boolean isAlarm() {
return false;
}
}
在场景类运行代码得到结果如下:
public class Client {
public static void main(String[] args) {
BaomaModel x5 = new BaomaX5();
BaomaModel x6 = new BaomaX6();
x5.run();
System.out.println("==============================");
x6.run();
}
}
//结果:
我是X5,我要发动了。。。
我要发动引擎了。。。
我要鸣笛,前面的家伙绕道。。。
我要停止了。。。
==============================
我是X6,我要发动了。。。
我要发动引擎了。。。
我要停止了。。。
在抽象类中定义了一个实现方法isAlarm()方法,子类可以重写该方法并决定是否鸣笛,其返回值会影响模板方法的执行结果,该方法就叫钩子方法(钩子函数)。这样的设计是非常优雅与有用的,值得我们细细揣摩。
模板方法模式的优点与使用场景
优点:
-
封装不变部分,扩展可变部分
-
提取公共部分代码,便于维护
-
行为由父类控制,子类实现
使用场景:
-
多个子类有公有的方法,并且逻辑基本相同
-
重要,复杂的算法,可以把核心算法设计为模板方法,周边的相关细节功能则由各个子类实现
参考
《设计模式之禅》