设计模式实战-模板方法模式
1 定义与类型
-
模板方法模式(Template Method Pattern)
Define the skeleton of an algorithm in an operation,deferring some steps to subclasses.Template Method lets subclasses redefine certain steps of an algorithm without changing the algorithm's structure.(定义一个操作中的算法的框架,而将一些步骤延迟到子类中。使得子类可以不改变一个算法的结构即可重定义该算法的某些特定步骤。)
定义
定义补充
类型
模板方法模式的通用类图
模板方法模式确实非常简单,仅仅使用了Java的继承机制,但它是一个应用非常广泛的模式。其中,AbstractClass叫做抽象模板,它的方法分为两类:
● 基本方法
基本操作,是由子类实现的方法,并且在模板方法被调用
● 模板方法
可以有一个或几个,一般是一个具体方法,也就是一个框架,实现对基本方法的调度,完成固定的逻辑
为了防止恶意的操作,一般模板方法都加上final关键字,不允许被覆写。
在类图中还有一个角色:具体模板。ConcreteClass1和ConcreteClass2属于具体模板,实现父类所定义的一个或多个抽象方法,也就是父类定义的基本方法在子类中得以实现。
我们来看其通用代码,AbstractClass抽象模板类
public abstract class AbstractClass {
//基本方法
protected abstract void doSomething();
//基本方法
protected abstract void doAnything();
//模板方法
public void templateMethod(){
/*
* 调用基本方法,完成相关的逻辑
*/
this.doAnything();
this.doSomething();
}
}
- 具体模板类
public class ConcreteClass1 extends AbstractClass {
//实现基本方法
protected void doAnything() {
//业务逻辑处理
}
protected void doSomething() {
//业务逻辑处理
}
}
public class ConcreteClass2 extends AbstractClass {
//实现基本方法
protected void doAnything() {
//业务逻辑处理
}
protected void doSomething() {
//业务逻辑处理
}
}
- 场景类
public class Client {
public static void main(String[] args) {
AbstractClass class1 = new ConcreteClass1();
AbstractClass class2 = new ConcreteClass2();
//调用模板方法
class1.templateMethod();
class2.templateMethod();
}
}
抽象模板中的基本方法尽量设计为protected类型,符合迪米特法则,不需要暴露的属性或方法尽量不要设置为protected类型。实现类若非必要,尽量不要扩大父类中的访问权限。
2 适用场景
场景● 多个子类有公有的方法,并且逻辑基本相同时。
● 重要、复杂的算法,可以把核心算法设计为模板方法,周边的相关细节功能则由各个子类实现。
● 重构时,模板方法模式是一个经常使用的模式,把相同的代码抽取到父类中,然后通过钩子函数(见“模板方法模式的扩展”)约束其行为。
3 优点
● 封装不变部分,扩展可变部分
把认为是不变部分的算法封装到父类实现,而可变部分的则可以通过继承来继续扩展
● 提取公共部分代码,便于维护
如果我们不抽取到父类中,任由这种散乱的代码发生,想想后果是什么样子?维护人员为了修正一个缺陷,需要到处查找类似的代码
● 行为由父类控制,子类实现
基本方法是由子类实现的,因此子类可以通过扩展的方式增加相应的功能,符合开闭原则
4 缺点
抽象类负责声明最抽象、最一般的事物属性和方法,实现类完成具体的事物属性和方法
但是模板方法模式却颠倒了,抽象类定义了部分抽象方法,由子类实现,子类执行的结果影响了父类的结果,也就是子类对父类产生了影响,这在复杂的项目中,会带来代码阅读的难度,而且也会让新手产生不适感。
相关设计模式
示例 - 辉煌工程——制造悍马
先不考虑扩展性,那好办,先按照最一般的经验设计类图
悍马车模型最一般的类图
- 非常简单的实现,悍马车有两个型号,H1和H2
按照需求,只需要悍马模型,那好我就给你悍马模型,先写个抽象类,然后两个不同型号的模型实现类,通过简单的继承就可以实现业务要求
我们先从抽象类开始编写
抽象悍马模型
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型号的悍马
public class HummerH1Model extends HummerModel {
//H1型号的悍马车鸣笛
public void alarm() {
System.out.println("悍马H1鸣笛...");
}
//引擎轰鸣声
public void engineBoom() {
System.out.println("悍马H1引擎声音是这样的...");
}
//汽车发动
public void start() {
System.out.println("悍马H1发动...");
}
//停车
public void stop() {
System.out.println("悍马H1停车...");
}
//开动起来
public void run(){
//先发动汽车
this.start();
//引擎开始轰鸣
this.engineBoom();
//然后就开始跑了,跑的过程中遇到一条狗挡路,就按喇叭
this.alarm();
//到达目的地就停车
this.stop();
}
}
注意run()方法,这是一个汇总的方法,一个模型生产成功,要拿给客户检测吧,怎么检测?
“是骡子是马,拉出去溜溜”,这就是一种检验方法,让它跑起来!
通过run()这样的方法,把模型的所有功能都测试到了。
- H2型号悍马
public class HummerH2Model extends HummerModel {
//H2型号的悍马车鸣笛
public void alarm() {
System.out.println("悍马H2鸣笛...");
}
//引擎轰鸣声
public void engineBoom() {
System.out.println("悍马H2引擎声音是这样在...");
}
//汽车发动
public void start() {
System.out.println("悍马H2发动...");
}
//停车
public void stop() {
System.out.println("悍马H2停车...");
}
//开动起来
public void run(){
//先发动汽车
this.start();
//引擎开始轰鸣
this.engineBoom();
//然后就开始跑了,跑的过程中遇到一条狗挡路,就按喇叭
this.alarm();
//到达目的地就停车
this.stop();
}
}
程序编写到这里,已经发现问题两个实现类的run()方法完全相同
那这个run()方法的实现应该出现在抽象类,不应该在实现类上,抽象是所有子类的共性封装
问题发现,马上更改
修改后的悍马车模类图
抽象类HummerModel#run()
,由抽象方法变更为实现方法
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();
}
}
在抽象的悍马模型上已经定义了run()方法的执行规则,先启动,然后引擎立刻轰鸣,中间还要按一下喇叭,然后停车
它的两个具体实现类就不需要实现run()方法了,只要把相应代码上的run()方法删除即可
场景类实现的任务就是把生产出的模型展现给客户
public class Client {
public static void main(String[] args) {
//XX公司要H1型号的悍马
HummerModel h1 = new HummerH1Model();
//H1模型演示
h1.run();
}
}
运行结果
悍马H1发动...
悍马H1引擎声音是这样的...
悍马H1鸣笛...
悍马H1停车...
目前客户只要看H1型号的悍马车,没问题,生产出来,同时可以运行起来给他看看。非常简单,那如果我告诉你这就是模板方法模式你会不会很不屑呢?就这模式,太简单了,我一直在使用呀!是的,你经常在使用,但你不知道这是模板方法模式,那些所谓的高手就可以很牛地说:“用模板方法模式就可以实现”,你还要很崇拜地看着,哇,牛人,模板方法模式是什么呀?这就是模板方法模式。
扩展
到目前为止,这两个模型都稳定地运行,突然有一天,老大急匆匆地找到了我:
“看你怎么设计的,车子一启动,喇叭就狂响,吵死人了!客户提出H1型号的悍马喇叭想让它响就响,H2型号的喇叭不要有声音,赶快修改一下。”
自己惹的祸,就要想办法解决它,稍稍思考一下,解决办法有了,先画出类图
类图改动似乎很小,在抽象类
HummerModel
中增加了一个实现方法isAlarm
,确定各个型号的悍马是否需要声音,由各个实现类覆写该方法,同时其他的基本方法由于不需要对外提供访问,因此也设计为protected类型
- 扩展后的抽象模板类
public abstract class HummerModel {
/*
* 首先,这个模型要能够被发动起来,别管是手摇发动,还是电力发动,反正
* 是要能够发动起来,那这个实现要在实现类里了
*/
protected abstract void start();
//能发动,还要能停下来,那才是真本事
protected abstract void stop();
//喇叭会出声音,是滴滴叫,还是哔哔叫
protected abstract void alarm();
//引擎会轰隆隆的响,不响那是假的
protected abstract void engineBoom();
//那模型应该会跑吧,别管是人推的,还是电力驱动,总之要会跑
final public void run() {
//先发动汽车
this.start();
//引擎开始轰鸣
this.engineBoom();
//要让它叫的就是就叫,喇嘛不想让它响就不响
if(this.isAlarm()){
this.alarm();
}
//到达目的地就停车
this.stop();
}
//钩子方法,默认喇叭是会响的
protected boolean isAlarm(){
return true;
}
}
-
在抽象类中,
isAlarm
是一个实现方法
模板方法根据其返回值决定是否要响喇叭,子类可以覆写该返回值,由于H1型号的喇叭是想让它响就响,不想让它响就不响,由人控制 -
扩展后的H1悍马
public class HummerH1Model extends HummerModel {
private boolean alarmFlag = true; //要响喇叭
protected void alarm() {
System.out.println("悍马H1鸣笛...");
}
protected void engineBoom() {
System.out.println("悍马H1引擎声音是这样的...");
}
protected void start() {
System.out.println("悍马H1发动...");
}
protected void stop() {
System.out.println("悍马H1停车...");
}
protected boolean isAlarm() {
return this.alarmFlag;
}
//要不要响喇叭,是由客户来决定的
public void setAlarm(boolean isAlarm){
this.alarmFlag = isAlarm;
}
}
只要调用H1型号的悍马,默认是有喇叭响的,当然你可以不让喇叭响,通过isAlarm(false)就可以实现。H2型号的悍马是没有喇叭声响的
- 扩展后的H2悍马
public class HummerH2Model extends HummerModel {
protected void alarm() {
System.out.println("悍马H2鸣笛...");
}
protected void engineBoom() {
System.out.println("悍马H2引擎声音是这样的...");
}
protected void start() {
System.out.println("悍马H2发动...");
}
protected void stop() {
System.out.println("悍马H2停车...");
}
//默认没有喇叭的
protected boolean isAlarm() {
return false;
}
}
H2型号的悍马设置isAlarm()的返回值为false,也就是关闭了喇叭功能
- 扩展后的场景类
public class Client {
public static void main(String[] args) throws IOException {
System.out.println("-------H1型号悍马--------");
System.out.println("H1型号的悍马是否需要喇叭声响?0-不需要 1-需要");
String type=(new BufferedReader(new InputStreamReader([System.in](http://system.in/)))).readLine();
HummerH1Model h1 = new HummerH1Model();
if(type.equals("0")){
h1.setAlarm(false);
}
h1.run();
System.out.println("\n-------H2型号悍马--------");
HummerH2Model h2 = new HummerH2Model();
h2.run();
}
}
运行是需要交互的,首先,要求输入H1型号的悍马是否有声音,如下所示:
-------H1型号悍马--------
H1型号的悍马是否需要喇叭声响?0-不需要 1-需要
输入“0”后的运行结果如下所示:
-------H1型号悍马--------
H1型号的悍马是否需要喇叭声响?0-不需要 1-需要
0
悍马H1发动...
悍马H1引擎声音是这样的...
悍马H1停车...
-------H2型号悍马--------
悍马H2发动...
悍马H2引擎声音是这样的...
悍马H2停车...
输入“1”后的运行结果如下所示:
-------H1型号悍马--------
H1型号的悍马是否需要喇叭声响?0-不需要 1-需要
1
悍马H1发动...
悍马H1引擎声音是这样的...
悍马H1鸣笛...
悍马H1停车...
-------H2型号悍马--------
悍马H2发动...
悍马H2引擎声音是这样的...
悍马H2停车...
看到没,H1型号的悍马是由客户自己控制是否要响喇叭,也就是说外界条件改变,影响到模板方法的执行。
在我们的抽象类中isAlarm的返回值就是影响了模板方法的执行结果,该方法就叫做钩子方法(Hook Method)。有了钩子方法模板方法模式才算完美,大家可以想想,由子类的一个方法返回值决定公共部分的执行结果,是不是很有吸引力呀!
模板方法模式就是在模板方法中按照一定的规则和顺序调用基本方法,具体到前面那个例子,就是run()方法按照规定的顺序(先调用start(),然后再调用engineBoom(),再调用alarm(),最后调用stop())调用本类的其他方法,并且由isAlarm()方法的返回值确定run()中的执行顺序变更。
Coding!!!
最佳实践
初级程序员在写程序的时候经常会问高手“父类怎么调用子类的方法”。这个问题很有普遍性,反正我是被问过好几回,那么父类是否可以调用子类的方法呢?我的回答是能,但强烈地、极度地不建议这么做,那该怎么做呢?
● 把子类传递到父类的有参构造中,然后调用
● 使用反射的方式调用
● 父类调用子类的静态方法
这三种都是父类直接调用子类的方法,好用不?好用!解决问题了吗?解决了!项目中允许使用不?不允许!
我就一直没有搞懂为什么要用父类调用子类的方法。如果一定要调用子类,那为什么要继承它呢?搞不懂。其实这个问题可以换个角度去理解,父类建立框架,子类在重写了父类部分的方法后,再调用从父类继承的方法,产生不同的结果(而这正是模板方法模式)。这是不是也可以理解为父类调用了子类的方法呢?你修改了子类,影响了父类行为的结果,曲线救国的方式实现了父类依赖子类的场景,模板方法模式就是这种效果。
模板方法在一些开源框架中应用非常多,它提供了一个抽象类,然后开源框架写了一堆子类。在《××× In Action》中就说明了,如果你需要扩展功能,可以继承这个抽象类,然后覆写protected方法,再然后就是调用一个类似execute方法,就完成你的扩展开发,非常容易扩展的一种模式。