概述
在现实生活中,很多事情都是有一定的规章制度的,有一定的流程,比如去外面吃饭都是要经过这个流程:排队,点单,吃饭,买单。一般来说都是这些流程,其实每个吃饭流程不同的就是,点单你点了什么不同的东西。
但是对于排队,吃饭,买单基本不同的地方也是一样的套路。
所以类比到软件开发中去,也会遇到这种情况:某个方法的实现需要多个步骤(外面吃饭),其中的几个步骤是一样的,固定不变的(排队,吃饭,买单),也有几个步骤其实具体的实现是不同的(点单),为了提高代码的复用性和系统的灵活性,可以使用一种称之为模板方法模式的设计模式来对这类情况进行设计,在模板方法模式中,将实现功能的每一个步骤所对应的方法称为基本方法,而调用这些基本方法同时定义基本方法的执行次序的方法称为模板方法。在模板方法模式中,可以将相同的代码放在父类中,对于不同的方法在父类中只做一个声明,将其具体实现放在不同的子类中。通过使用模板方法模式,一方面提高了代码的复用性,另一方面还可以利用面向对象的多态性,在运行时选择一种具体子类,实现完整的“请客”方法,提高系统的灵活性和可扩展性。
概念
模板方法模式:定义一个操作中算法的框架,而将一些步骤延迟到子类中。模板方法模式使得子类可以不改变一个算法的结构即可重定义该算法的某些特定步骤。
模板方法模式实现
在这里我们就用咖啡和茶两种饮料的冲泡方法来举例子
咖啡 | 红茶 |
---|---|
把水煮沸 | 把水煮沸 |
用沸水冲泡咖啡 | 用沸水浸泡茶叶 |
把咖啡倒入杯中 | 把红茶倒入杯中 |
加糖和牛奶 | 加柠檬 |
其实可以抽象成4步骤,不管是咖啡还是红茶都是这四步
- 把水煮沸(boilWater)
- 泡饮料(brew)
- 把饮料倒进杯子(pourInCup)
- 加调味料(addCondiments)
所以按照模板方法模式的概念我们要定义一个抽象基类来规范方法。
RefreshBeverage.java
package com.xjh.template;
/**
* 抽象基类
* @author Gin
*
*/
public abstract class RefreshBeverage {
/*
* 封装所有子类共同遵循的算法框架
*/
public final void RefreshBeverageTemplate() {
// 1. 把水煮沸
boilWater();
// 2. 泡饮料
brew();
// 3. 把饮料倒进杯子
pourInCup();
// 4. 加调味料
addCondiments();
}
// 1. 把水煮沸(基本方法)
private void boilWater() {
// TODO Auto-generated method stub
System.out.println("把水煮沸");
}
// 2. 泡饮料(抽象基本方法)
protected abstract void brew();
// 3. 把饮料倒进杯子(基本方法)
private void pourInCup() {
// TODO Auto-generated method stub
System.out.println("把饮料倒进杯子");
}
// 4. 加调味料(抽象基本方法)
protected abstract void addCondiments();
}
当然在这里面就有可以思考的东西了,我们可以发现我们的基本方法都是private的,但是抽象基本方法都是protected,这是为什么呢?其实是这样的,因为我们的共用方法是固定的,所以并不需要对外暴露,而抽象方法需要交给子类去写,但是也不想外部可见,所以使用了protected。
修饰符 | 当前类 | 同 包 | 子 类 | 其他包 |
---|---|---|---|---|
public | √ | √ | √ | √ |
protected | √ | √ | √ | × |
default | √ | √ | × | × |
private | √ | × | × | × |
类的成员不写访问修饰时默认为default。默认对于同一个包中的其他类相当于公开(public),对于不是同一个包中的其他类相当于私有(private)。受保护(protected)对子类相当于公开,对不是同一包中的没有父子关系的类相当于私有。Java中,外部类的修饰符只能是public或默认,类的成员(包括内部类)的修饰符可以是以上四种。
那为什么我们的调用方法要用final呢?这就是因为final最终方法 ,不可被修改,以免干扰到模板的实现。
接下来就是实现Coffer和Tea子类。
主要是将两个不同的方法进行实现。
Coffer.java
package com.xjh.template;
public class Coffer extends RefreshBeverage {
@Override
protected void brew() {
// TODO Auto-generated method stub
System.out.println("用沸水冲泡咖啡");
}
@Override
protected void addCondiments() {
// TODO Auto-generated method stub
System.out.println("加糖和牛奶");
}
}
Tea.java
package com.xjh.template;
public class Tea extends RefreshBeverage {
@Override
protected void brew() {
// TODO Auto-generated method stub
System.out.println("用沸水浸泡茶叶");
}
@Override
protected void addCondiments() {
// TODO Auto-generated method stub
System.out.println("加柠檬");
}
}
然后我们通过直接调用制作方法就可以了。这就是面向接口编程而不是面向实现编程。
RefreshBeverageTest.java
package com.xjh.template;
public class RefreshBeverageTest {
public static void main(String[] args) {
// TODO Auto-generated method stub
RefreshBeverage coffer = new Coffer();
System.out.println("开始制作咖啡");
coffer.RefreshBeverageTemplate();
System.out.println("结束制作咖啡");
System.out.println("--------------------------------");
RefreshBeverage tea = new Tea();
System.out.println("开始泡制茶");
tea.RefreshBeverageTemplate();
System.out.println("结束泡制茶");
}
}
但是我们会发现,其实有些人不喜欢加调味料,但是我们现在的实现是不能满足要求的,所以我们就要使用一种叫钩子方法的概念:提供一个默认或空的实现,具体的子类可以自行决定是否挂钩以及如何挂钩。
RefreshBeverage.java
/*
* Hook,钩子函数
* 提供一个默认或空的实现,具体的子类可以自行决定是否挂钩以及如何挂钩
* 询问是否加入调料
*/
protected boolean isCustomerWantsCondiments() {
return true;
}
在制作方法中加入判断是否加入调味料。
public final void RefreshBeverageTemplate() {
// 1. 把水煮沸
boilWater();
// 2. 泡饮料
brew();
// 3. 把饮料倒进杯子
pourInCup();
// 4. 加调味料
if(isCustomerWantsCondiments()) {
addCondiments();
}
}
然后我们在定义一个BlackCoffer子类进行实现
BlackCoffer.java
package com.xjh.template;
public class BlackCoffer extends RefreshBeverage {
@Override
protected void brew() {
// TODO Auto-generated method stub
System.out.println("用沸水冲泡咖啡");
}
@Override
protected void addCondiments() {
// TODO Auto-generated method stub
System.out.println("无");
}
/*
* 子类通过覆盖的形式选择挂载钩子函数
* @see com.xjh.template.RefreshBeverage#isCustomerWantsCondiments()
*/
protected boolean isCustomerWantsCondiments() {
return false;
}
}
这样我们就实现了相关操作,但是现在这个实现可能会有人说我直接在addCondiments方法中不输出就可以了,因为都是要实现的,那为什么要这样呢,其实这更多是一个判断,就比如我想的:在new一个coffer对象的时候传入要不要调味料,定义一个标志位,然后我们根据不同的情况返回这个标志位就可以了,这样是不是很灵活呢,但是我不确定这个是不是钩子函数,但是在我理解下感觉思路和imooc上老师的这个思路意义差不多。但是我不确定,所以这里还是用imooc上的老师的方法了。
总结
我们有两个重要的部分,一个是抽象基类,一个是具体子类。
在抽象基类中我们要提供基本方法,抽象方法,钩子函数(可选)以及模板方法(final)。
在具体子类中我们要实现基类中的抽象方法,可选的钩子方法
模板方法就是准备一个抽象类,将部分逻辑以具体方法的形式实现,然后声明一些抽象方法交由子类实现剩余逻辑,用钩子方法给予子类更大的灵活性,最后将方法汇总构成一个不可改变的模板方法。
适用场景
- 算法或操作遵循相似的逻辑
- 重构时(把相同的代码抽取到父类中)
- 重要、复杂的算法,核心算法设计为模板算法
优点和缺点
优点
- 封装性好
- 复用性高
- 屏蔽细节
- 便于维护
缺点:继承的唯一性