定义
定义一个操作中的算法骨架,而将算法的一些步骤延迟到子类中,使得子类可以不改变该算法结构的情况下重定义该算法的某些特定步骤。它是一种类行为型模式。
- 它封装了不变部分,扩展可变部分。它把认为是不变部分的算法封装到父类中实现,而把可变部分算法由子类继承实现,便于子类继续扩展。
- 它在父类中提取了公共的部分代码,便于代码复用。
- 部分方法是由子类实现的,因此子类可以通过扩展方式增加相应的功能,符合开闭原则。
该模式的主要缺点如下。
- 对每个不同的实现都需要定义一个子类,这会导致类的个数增加,系统更加庞大,设计也更加抽象。
- 父类中的抽象方法由子类实现,子类执行的结果会影响父类的结果,这导致一种反向的控制结构,它提高了代码阅读的难度。
模式的结构与实现
模板方法模式需要注意抽象类与具体子类之间的协作。它用到了虚函数的多态性技术以及“不用调用我,让我来调用你”的反向控制技术。现在来介绍它们的基本结构。
- 对每个不同的实现都需要定义一个子类,这会导致类的个数增加,系统更加庞大,设计也更加抽象。
- 父类中的抽象方法由子类实现,子类执行的结果会影响父类的结果,这导致一种反向的控制结构,它提高了代码阅读的难度。
1. 模式的结构
模板方法模式包含以下主要角色。
(1) 抽象类(Abstract Class):负责给出一个算法的轮廓和骨架。它由一个模板方法和若干个基本方法构成。这些方法的定义如下。
① 模板方法:定义了算法的骨架,按某种顺序调用其包含的基本方法。
② 基本方法:是整个算法中的一个步骤,包含以下几种类型。
(2) 具体子类(Concrete Class):实现抽象类中所定义的抽象方法和钩子方法,它们是一个顶级逻辑的一个组成步骤。
基本方法: 是整个算法中的一个步骤,包含以下几种类型。
- 抽象方法:父类中只声明但不加以实现,而是定义好规范,然后由它的子类去实现。
- 具体方法:由抽象类声明并加以实现。一般来说,模版方法调用抽象方法来完成主要的逻辑功能,并且,模版方法大多会定义为final类型,指明主要的逻辑功能在子类中不能被重写。
- 钩子方法:由抽象类声明并加以实现。但是子类可以去扩展,子类可以通过扩展钩子方法来影响模版方法的逻辑。
2. 模式的实现
饮料制法:
把水煮沸(boilWater)
冲饮料(brew)
把饮料倒进杯子(pourInCup)
加调味料(addCondiments)
抽象基类:Drinks
package com.hcx.pattern.template;
/**
* 抽象基类,为所有子类提供一个算法框架
* 饮料
* @author HCX
*
*/
public abstract class Drinks {
/**
* 使用final修饰,防止子类改变模版方法
* 制备饮料的模版方法
* 封装了所有子类共同遵循的算法框架
*/
public final void prepareDrinksTemplate(){
//步骤一:把水煮沸
boilWater();
//步骤二:冲饮料
brew();
//步骤三:把饮料倒进杯子
pourInCup();
//步骤四:加调味料
addCondiments();
}
/**
* 基本方法:把水煮沸
* 对所有子类,是一个共同的行为,不需要向子类开放;将变化的东西放在高层代码中。
*/
private void boilWater() {
System.out.println("把水煮沸");
}
/**
* 基本方法:将饮料倒入杯中
*/
private void pourInCup() {
System.out.println("将饮料倒入杯中");
}
/**
* 不同的情况,具体的实现不同,设计为抽象方法,需要在子类中可见,以便子类复写,提供具体的实现。
* 抽象的基本方法:加入调料
*/
protected abstract void addCondiments();
/**
* 抽象的基本方法:泡饮料
*/
protected abstract void brew();
}
具体子类:Coffee
package com.hcx.pattern.template;
/**
* 具体子类,提供了咖啡制备的具体实现
* @author HCX
*
*/
public class Coffee extends Drinks {
@Override
protected void brew() {
System.out.println("用沸水冲泡咖啡");
}
@Override
protected void addCondiments() {
System.out.println("加入糖和牛奶");
}
}
具体子类:OrangeJuice
package com.hcx.pattern.template;
/**
* 具体子类,提供了橙汁的具体实现
* @author HCX
*
*/
public class OrangeJuice extends Drinks{
@Override
protected void brew() {
System.out.println("准备橙子和榨汁机,把橙子丢入机器中榨汁");
}
@Override
protected void addCondiments() {
System.out.println("加入糖浆");
}
}
测试:
package com.hcx.pattern.template;
public class DrinksTest {
public static void main(String[] args) {
System.out.println("咖啡制备中");
Drinks drinks = new Coffee();
drinks.prepareDrinksTemplate();
System.out.println("咖啡好了");
System.out.println("*************************************");
System.out.println("橙汁制备中");
Drinks drinks2 = new OrangeJuice();
drinks2.prepareDrinksTemplate();
System.out.println("橙汁好了");
}
}
结果:
咖啡制备中
把水煮沸
用沸水冲泡咖啡
将饮料倒入杯中
加入糖和牛奶
咖啡好了
*************************************
橙汁制备中
把水煮沸
准备榨汁机和榨汁机,把橙子丢入机器中榨汁
将饮料倒入杯中
加入糖浆
橙汁好了
使用钩子方法使代码更灵活:
在制备橙汁时,不想加入糖浆;
修改Drinks类,在加入调味料的步骤进行判断,编写钩子函数:
package com.hcx.pattern.template;
/**
* 抽象基类,为所有子类提供一个算法框架
* 饮料
* @author HCX
*
*/
public abstract class Drinks {
/**
* 使用final修饰,防止子类改变模版方法
* 制备饮料的模版方法
* 封装了所有子类共同遵循的算法框架
*/
public final void prepareDrinksTemplate(){
//步骤一:把水煮沸
boilWater();
//步骤二:冲饮料
brew();
//步骤三:把饮料倒进杯子
pourInCup();
//步骤四:加调味料
if(wantCondiments()){
addCondiments();
}
}
/**
* Hook:钩子函数,提供一个默认或空的实现
* 具体的子类可以自行决定是否挂钩以及如何挂钩,即是否重写父类的钩子函数
* 根据个人喜好,是否加入调料
* @return
*/
protected boolean wantCondiments() {
return true;
}
/**
* 基本方法:把水煮沸
* 对所有子类,是一个共同的行为,不需要向子类开放;将变化的东西放在高层代码中。
*/
private void boilWater() {
System.out.println("把水煮沸");
}
/**
* 基本方法:将饮料倒入杯中
*/
private void pourInCup() {
System.out.println("将饮料倒入杯中");
}
/**
* 不同的情况,具体的实现不同,设计为抽象方法,需要在子类中可见,以便子类复写,提供具体的实现。
* 抽象的基本方法:加入调料
*/
protected abstract void addCondiments();
/**
* 抽象的基本方法:泡饮料
*/
protected abstract void brew();
}
在OrangeJuice中重写钩子函数时:
package com.hcx.pattern.template;
/**
* 具体子类,提供了橙汁的具体实现
* @author HCX
*
*/
public class OrangeJuice extends Drinks{
@Override
protected void brew() {
System.out.println("准备橙子和榨汁机,把橙子丢入机器中榨汁");
}
@Override
protected void addCondiments() {
System.out.println("加入糖浆");
}
/**
* 重写父类的钩子方法
* 不加入任何调料,纯正的橙汁
*/
@Override
protected boolean wantCondiments() {
return false;
}
}
测试类打印结果:
咖啡制备中
把水煮沸
用沸水冲泡咖啡
将饮料倒入杯中
加入糖和牛奶
咖啡好了
*************************************
橙汁制备中
把水煮沸
准备橙子和榨汁机,把橙子丢入机器中榨汁
将饮料倒入杯中
橙汁好了
模版方法的优点及适用场景
容易扩展。一般来说,抽象类中的模版方法是不易反生改变的部分,而抽象方法是容易反生变化的部分,因此通过增加实现类一般可以很容易实现功能的扩展,符合开闭原则。
便于维护。对于模版方法模式来说,正是由于他们的主要逻辑相同,才使用了模版方法,假如不使用模版方法,任由这些相同的代码散乱的分布在不同的类中,维护起来是非常不方便的。
比较灵活。因为有钩子方法,因此,子类的实现也可以影响父类中主逻辑的运行。但是,在灵活的同时,由于子类影响到了父类,违反了里氏替换原则,也会给程序带来风险。这就对抽象类的设计有了更高的要求。
在多个子类拥有相同的方法,并且这些方法逻辑相同时,可以考虑使用模版方法模式。在程序的主框架相同,细节不同的场合下,也比较适合使用这种模式。
参考于: