定义:定义一个操作中算法的框架,而将一些步骤延迟到子类中,使得子类可以不改变算法的结构即可重定义该算法中的某些特定步骤。
模板方法模式相对而言比较简单,一般的都是由抽象类定义好模板方法,
然后,子类通过继承并实现其父类中定义好的模板中需要执行的具体的方法,调用子类对象的模板方法时,会执行该类中的具体实现的方法。
这个模式我个人的感觉有点像是面向过程的操作,执行完一道工序,接着下一道工序。
类型:行为类模式
类图:
例子:
汽车厂造悍马
假设我们是一个汽车公司,现在有客户来了,要求我们造悍马! 既然上级下来命令那就造呗,但是造悍马你得告诉我们汽车有什么功能啊,客户说了:“能启动车,能停止车,能响,能跑。”好,功能出来了,开始造汽车了。类图如下:
抽象悍马模型代码如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
|
<code>
public
abstract
class
HummerModel {
/*
* 首先,这个模型要能够被发动起来,别管是手摇发动,还是电力发动,反正
* 是要能够发动起来,那这个实现要在实现类里了
*/
public
abstract
void
start();
//能发动,还要能停下来,那才是真本事
public
abstract
void
stop();
//喇叭会出声音,是滴滴叫,还是哔哔叫
public
abstract
void
alarm();
//引擎会轰隆隆地响,不响那是假的
public
abstract
void
engineBoom();
//那模型应该会跑吧,别管是人推的,还是电力驱动的,总之要会跑
public
abstract
void
run();
}
</code>
|
在抽象类中,我们定义了悍马模型都必须具有的特质:能够发动、停止,喇叭会响,引擎可以轰鸣,而且还可以停止。但是每个型号的悍马实现是不同的。
H1型号的悍马代码下所示:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
|
<code>
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();
}
}
</code>
|
大家注意看run()方法,这是一个汇总的方法,一个模型生产成功了,总要拿给客户检测吧,怎么检测?“是骡子是马,拉出去溜溜”,这就是一种检验方法,让它跑起来!通过run()这样的方法,把模型的所有功能都测试到了。
H2型号悍马如下代码所示。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
|
<code>
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();
}
}
</code>
|
好了,程序编写到这里,已经发现问题了,两个实现类的run()方法都是完全相同的,那这个run()方法的实现应该出现在抽象类,不应该在实现类上,抽象是所有子类的共性封装。
注意 在软件开发过程中,如果相同的一段代码复制过两次,就需要对设计产生怀疑, 架构师要明确地说明为什么相同的逻辑要出现两次或更多次。
好,问题发现了,我们就需要马上更改,修改后的类图下所示:
注意 抽象类HummerModel中的run()方法,由抽象方法变更为实现方法,其源代码如下所示。<喎�"/kf/ware/vc/" target="_blank" class="keylink">vcD4NCjxoMyBpZD0="修改后的抽象悍马模型">修改后的抽象悍马模型
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
|
<code>
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();
}
}
</code>
|
场景类实现的任务就是把生产出的模型展现给客户,场景类代码如下:
1
2
3
4
5
6
7
8
9
|
<code>
public
class
Client {
public
static
void
main(String[] args) {
//XX公司要H1型号的悍马
HummerModel h1 =
new
HummerH1Model();
//H1模型演示
h1.run();
}
}
</code>
|
运行结果如下所示。
悍马H1发动… 悍马H1引擎声音是这样的… 悍马H1鸣笛…
悍马H1停车…
目前客户只要看H1型号的悍马车,没问题,生产出来,同时可以运行起来给他看看。 非常简单,那如果我告诉你这就是模板方法模式你会不会很不屑呢?就这模式,太简单了, 我一直在使用呀!是的,你经常在使用,但你不知道这是模板方法模式,那些所谓的高手就可以很牛地说:“用模板方法模式就可以实现”,你还要很崇拜地看着,哇,牛人,模板方法 模式是什么呀?这就是模板方法模式。
注意 为了防止恶意的操作,一般模板方法都加上final关键字,不允许被覆写。 抽象模板中的基本方法尽量设计为protected类型,符合迪米特法则,不需要暴露的属性或方法尽量不要设置为protected类型。实现类若非必要,尽量不要扩大父类中的访问权限。
模板方法模式的使用场景
● 多个子类有公有的方法,并且逻辑基本相同时。
● 重要、复杂的算法,可以把核心算法设计为模板方法,周边的相关细节功能则由各个 子类实现。
● 重构时,模板方法模式是一个经常使用的模式,把相同的代码抽取到父类中,然后通 过钩子函数(见“模板方法模式的扩展”)约束其行为。
模板方法模式的扩展
目前为止,这两个模型都稳定地运行,突然有一天,领导急匆匆地找到了我:“看你怎么设计的,车子一启动,喇叭就狂响,吵死人了!客户提出H1型号的悍马喇叭 想让它响就响,H2型号的喇叭不要有声音,赶快修改一下。”
自己惹的祸,就要想办法解决它,稍稍思考一下,解决办法有了,先画出类图如下:
类图改动似乎很小,在抽象类HummerModel中增加了一个实现方法isAlarm,确定各个型 号的悍马是否需要声音,由各个实现类覆写该方法,同时其他的基本方法由于不需要对外提 供访问,因此也设计为protected类型。其源代码如下所示:
扩展后的抽象模板类
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
|
<code>
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
;
}
}
</code>
|
在抽象类中,isAlarm是一个实现方法。其作用是模板方法根据其返回值决定是否要响喇叭,子类可以覆写该返回值,由于H1型号的喇叭是想让它响就响,不想让它响就不响,由人控制,其源代码如下所示。
扩展后的H1悍马
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
|
<code>
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;
}
}
</code>
|
只要调用H1型号的悍马,默认是有喇叭响的,当然你可以不让喇叭响,通过isAlarm(false)就可以实现。H2型号的悍马是没有喇叭声响的,其源代码如代码下所示。
扩展后的H2悍马
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
<code>
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
;
}
}
</code>
|
H2型号的悍马设置isAlarm()的返回值为false,也就是关闭了喇叭功能。场景类代码如下所示。
扩展后的场景类
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
<code>
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))). HummerH1Model h1 =
new
HummerH1Model();
if
(type.equals(
"0"
)){
h1.setAlarm(
false
);
}
h1.run();
System.out.println(
"\n-------H2型号悍马--------"
);
HummerH2Model h2 =
new
HummerH2Model();
h2.run();
}
}
</code>
|
运行是需要交互的,首先,要求输入H1型号的悍马是否有声音,如下所示:
1
2
3
4
5
6
|
<code>-------H1型号悍马-------- H1型号的悍马是否需要喇叭声响?
0
-不需要
1
-需要输入“
0
”后的运行结果如下所示:
-------H1型号悍马-------- H1型号的悍马是否需要喇叭声响?
0
-不需要
1
-需要
0
悍马H1发动... 悍马H1引擎声音是这样的... 悍马H1停车...
-------H2型号悍马-------- 悍马H2发动... 悍马H2引擎声音是这样的... 悍马H2停车...
</code>
|
输入“1”后的运行结果如下所示:
1
2
3
4
5
6
7
8
|
<code>-------H1型号悍马-------- H1型号的悍马是否需要喇叭声响?
0
-不需要
1
-需要
1
悍马H1发动...
悍马H1引擎声音是这样的...
悍马H1鸣笛...
悍马H1停车...
-------H2型号悍马-------- 悍马H2发动... 悍马H2引擎声音是这样的... 悍马H2停车...
</code>
|
看到没,H1型号的悍马是由客户自己控制是否要响喇叭,也就是说外界条件改变,影响到模板方法的执行。在我们的抽象类中isAlarm的返回值就是影响了模板方法的执行结 果,该方法就叫做钩子方法(Hook Method)。有了钩子方法模板方法模式才算完美,大家 可以想想,由子类的一个方法返回值决定公共部分的执行结果,是不是很有吸引力呀!
模板方法模式就是在模板方法中按照一定的规则和顺序调用基本方法,具体到前面那个例子,就是run()方法按照规定的顺序(先调用start(),然后再调用engineBoom(),再调用 alarm(),最后调用stop())调用本类的其他方法,并且由isAlarm()方法的返回值确定run()中的 执行顺序变更。
模版方法模式的结构
模版方法模式由一个抽象类和一个(或一组)实现类通过继承结构组成,抽象类中的方法分为三种:
- 抽象方法:父类中只声明但不加以实现,而是定义好规范,然后由它的子类去实现。
- 模版方法:由抽象类声明并加以实现。一般来说,模版方法调用抽象方法来完成主要的逻辑功能,并且,模版方法大多会定义为final类型,指明主要的逻辑功能在子类中不能被重写。
- 钩子方法:由抽象类声明并加以实现。但是子类可以去扩展,子类可以通过扩展钩子方法来影响模版方法的逻辑。
- 抽象类的任务是搭建逻辑的框架,通常由经验丰富的人员编写,因为抽象类的好坏直接决定了程序是否稳定性。
实现类用来实现细节。抽象类中的模版方法正是通过实现类扩展的方法来完成业务逻辑。只要实现类中的扩展方法通过了单元测试,在模版方法正确的前提下,整体功能一般不会出现大的错误。
模版方法的优点及适用场景
容易扩展。一般来说,抽象类中的模版方法是不易反生改变的部分,而抽象方法是容易反生变化的部分,因此通过增加实现类一般可以很容易实现功能的扩展,符合开闭原则。
便于维护。对于模版方法模式来说,正是由于他们的主要逻辑相同,才使用了模版方法,假如不使用模版方法,任由这些相同的代码散乱的分布在不同的类中,维护起来是非常不方便的。
比较灵活。因为有钩子方法,因此,子类的实现也可以影响父类中主逻辑的运行。但是,在灵活的同时,由于子类影响到了父类,违反了里氏替换原则,也会给程序带来风险。这就对抽象类的设计有了更高的要求。
在多个子类拥有相同的方法,并且这些方法逻辑相同时,可以考虑使用模版方法模式。在程序的主框架相同,细节不同的场合下,也比较适合使用这种模式