模板方法定义:
在一个方法中定义一个算法的骨架,而将一些步骤延迟到子类中。模板方法使得子类可以在不改变算法结构的情况下,重新定义算法中的某些步骤。
UML图:
实例场景:
headfirst里面是这样的一个例子:
在一个店里面,有2种饮料,它们的冲泡步骤是这样的:
- 咖啡:把水煮沸,用沸水冲泡咖啡,倒进杯子,加糖和牛奶
- 茶:把水煮沸,用沸水浸泡茶叶,倒进杯子,加柠檬
/**
* 咖啡
*/
public class Coffee {
/**
* 准备
*/
public void prepare() {
boilWater();//把水煮沸
brewCoffeeGrinds();//冲泡咖啡
pourInCup();//倒进杯子
addSugarAndMilk();//添加糖和牛奶
}
/**
* 把水煮沸
*/
private void boilWater() {
System.out.println("把水煮沸");
}
/**
* 冲泡咖啡
*/
private void brewCoffeeGrinds() {
System.out.println("用沸水冲泡咖啡");
}
/**
* 倒进杯子
*/
private void pourInCup() {
System.out.println("倒进杯子");
}
/**
* 添加糖和牛奶
*/
private void addSugarAndMilk() {
System.out.println("添加糖和牛奶");
}
}
/**
* 茶
*/
public class Tea {
/**
* 准备
*/
public void prepare() {
boilWater();//把水煮沸
steepTeaBag();//泡茶
pourInCup();//倒进杯子
addLemon();//加柠檬
}
/**
* 把水煮沸
*/
private void boilWater() {
System.out.println("把水煮沸");
}
/**
* 泡茶
*/
private void steepTeaBag() {
System.out.println("用沸水浸泡茶叶");
}
/**
* 倒进杯子
*/
private void pourInCup() {
System.out.println("倒进杯子");
}
/**
* 加柠檬
*/
private void addLemon() {
System.out.println("添加柠檬");
}
}
上面贴了咖啡和茶的实现,对外提供的public方法是prepare()方法,其他的内部方法,都是private,按道理来说,上面两段代码,思路清晰,注释完整,代码整洁。
但是,boilWater(),pourInCup()两个方法,其实内容是一模一样的,对于一个程序来说,有2段一模一样的代码的时候,就应该思考,是不是有什么地方不对。因为,有2段就表示要改2个同样的地方,有10段,就要改10个同样的地方,System.out.println()当然能改啊。
逻辑一多咋办?逻辑一多当然也能改啊,毕竟总所周知,程序员是只需要3个键(Ctrl,C,V)就能正常工作的。但是,还有键盘在别人手上啊,他们写在什么地方的知道不?
2,逻辑抽象第一版
逻辑上来说,这个地方就会被抽象出一个公共类:
/**
* 咖啡因的饮料(将烧水和倒进杯子两个方法抽象出来)
*/
public abstract class CaffeineBeverage {
public abstract void prepare();//子类必须要有一个准备饮料的方法
/**
* 把水煮沸
*/
protected void boilWater() {
System.out.println("把水煮沸");
}
/**
* 倒进杯子
*/
protected void pourInCup() {
System.out.println("倒进杯子");
}
}
咖啡和茶的实现就会变成下面这样:
/**
* 咖啡
*/
public class Coffee extends CaffeineBeverage{
/**
* 准备
*/
public void prepare() {
boilWater();//把水煮沸
brewCoffeeGrinds();//冲泡咖啡
pourInCup();//倒进杯子
addSugarAndMilk();//添加糖和牛奶
}
/**
* 冲泡咖啡
*/
private void brewCoffeeGrinds() {
System.out.println("用沸水冲泡咖啡");
}
/**
* 添加糖和牛奶
*/
private void addSugarAndMilk() {
System.out.println("添加糖和牛奶");
}
}
/**
* 茶
*/
public class Tea extends CaffeineBeverage{
/**
* 准备
*/
public void prepare() {
boilWater();//把水煮沸
steepTeaBag();//泡茶
pourInCup();//倒进杯子
addLemon();//加柠檬
}
/**
* 泡茶
*/
private void steepTeaBag() {
System.out.println("用沸水浸泡茶叶");
}
/**
* 加柠檬
*/
private void addLemon() {
System.out.println("添加柠檬");
}
}
一般来说,实际业务中,抽象到这个地方,已经比复制粘贴的时候好很多了。但是,很多时候,不能只看表面,还需要总结更加深层次的东西,让业务的实现变得更加简单。
比如在这个例子中,prepare()方法中的步骤还可以总结,抽象。
原来的冲泡步骤:
- 咖啡:把水煮沸,用沸水冲泡咖啡,倒进杯子,加糖和牛奶
- 茶:把水煮沸,用沸水浸泡茶叶,倒进杯子,加柠檬
抽象后:
咖啡/茶:把水煮沸,用沸水 【冲泡咖啡/浸泡茶叶】,倒进杯子,加 【糖和牛奶/柠檬】
第2步和第4步,还可以抽象成,冲泡,加调味品
3,模板方法模式抽象
那么抽象类的代码就会变成这样:
/**
* 咖啡因的饮料(模板方法模式)
*/
public abstract class CaffeineBeverage {
/**
* 准备(构造成final方法,防止子类重写算法)
*/
final void prepare() {
boilWater();
brew();
pourInCup();
addCondiments();
}
/**
* 冲泡
*/
abstract void brew();
/**
* 添加调料
*/
abstract void addCondiments();
/**
* 把水煮沸
*/
public void boilWater() {
System.out.println("把水煮沸");
}
/**
* 倒进杯子
*/
public void pourInCup() {
System.out.println("倒进杯子");
}
}
咖啡和茶的实现如下:
/**
* 咖啡
*/
public class Coffee extends CaffeineBeverage {
/**
* 冲泡咖啡
*/
void brew() {
System.out.println("用沸水冲泡咖啡");
}
/**
* 添加糖和牛奶
*/
void addCondiments() {
System.out.println("添加糖和牛奶");
}
}
/**
* 茶
*/
public class Tea extends CaffeineBeverage {
/**
* 泡茶
*/
void brew() {
System.out.println("用沸水浸泡茶叶");
}
/**
* 加柠檬
*/
void addCondiments() {
System.out.println("添加柠檬");
}
}
测试类:
/**
* 测试类
*/
public class Test {
public static void main(String[] args) {
Tea tea = new Tea();
Coffee coffee = new Coffee();
System.out.println("泡茶...");
tea.prepare();
System.out.println("冲咖啡...");
coffee.prepare();
}
}
这个就是一个模板方法模式比较通用的一个实现了,中间就有模板方法模式的2个要点:
- 有一个由多个步骤构成的方法,这里就是prepare(),由4个步骤构成的一个模板方法
- 子类可以自行实现其中的一个或多个步骤,咖啡和茶分别都实现了brew(),addCondiments()
其实到这个地方呢,模板方法模式的一般逻辑就大概讲完了,一般来说,实现也就是上面的那个样子,但是,还是有很多时候,会出现各种各样的其他实现方式,毕竟抽象这个东西,对于不同的业务,不同的逻辑,那简直就是多种多样,只要不违背设计原则,简单易用,那么总会有意识无意识的用到模板方法模式。
接下来就介绍几种常见的操作。
1,空实现
比如说,在把水煮沸前有一个前置步骤,有的饮料需要,但是有的饮料不需要,那么就可以在模板方法里面,给它留一个位置,让子类去选择性的覆盖实现
/**
* 咖啡因的饮料(模板方法模式,空实现)
*/
public abstract class CaffeineBeverage {
/**
* 准备(构造成final方法,防止子类重写算法)
*/
final void prepare() {
beforeBoilWater();
boilWater();
brew();
pourInCup();
addCondiments();
}
/**
* 烧水前置操作
*/
protected void beforeBoilWater(){
}
//其他方法省略...
}
2,默认实现
比如说,加调味品这个步骤其实是可选的,加不加调味品,每种饮料加不加调味品,模板方法可以交给子类自己去控制。
/**
* 咖啡因的饮料(模板方法模式,默认实现)
*/
public abstract class CaffeineBeverage {
/**
* 准备(构造成final方法,防止子类重写算法)
*/
final void prepare() {
boilWater();
brew();
pourInCup();
if(needCondiments()){
addCondiments();
}
}
/**
* 是否需要调味品
*/
protected boolean needCondiments(){
return false;
}
//其他方法省略...
}
这既是模板方法模式的第3个要点,架构允许的情况下,子类可以重新定义某些步骤,只要模板方法可以定义添加或者移除某个,某些步骤,那么子类就可以根据自己的实际情况来选择实现整个方法的逻辑步骤。这个也就是这个设计模式中常说的一种叫钩子方法。
钩子是一种被声明在抽象类中的方法,但只有空的或者默认的实现。钩子的存在,可以让子类有能力对算法的不同点进行挂钩。要不要挂钩,由子类自行决定。
当你的子类“必须”提供算法中某个方法或者步骤的实现时,就使用抽象方法。如果算法的这个部分是可选的,就使用钩子。如果是钩子的话,子类可以选择实现这个钩子,但并不强制这么做。
总结
模板方法模式,其实就是,在抽象类中定义一个操作中的算法的步骤,而将一些步骤的实现延迟到子类中。
这里有一个建议是,尽量在抽象的时候,保证仅存在父类的方法去调用子类的方法,而不要同时存在子类的方法去调用父类的方法。
个人觉得,这样做的原因有几点:
- 贯彻这个建议后,整个逻辑会显得很简单易懂,反正没有实现的,在子类就能找到实现
- 相互依赖调来调去的后果就是在系统越来越复杂以后,最后就没人能看懂了
- 防止抽象类的方法变动引起子类的改动,这个其实不算原因,因为一般来说,抽象类是比较稳定的,而且,子类可以调的抽象类方法在修改时肯定要考量子类的,不允许子类调用的方法肯定都处理过了,一般肯定是调不到的(诶,反射你凑过来干嘛?)
最后的最后,总结一下模板方法模式的优缺点吧
优点:
- 代码复用便于维护,子类可扩展
- 行为由父类控制,子类只需要关心自己所需要的步骤即可,开发难度低
缺点:
每一个不同的实现都需要一个子类来实现,导致类的个数增加,使得系统更加庞大,所以,如果发现子类很多,是不是要想想是不是设计模式用错了,去隔壁找找其他的设计模式吧。
参考:https://www.cnblogs.com/skyseavae/p/10633729.html