模板模式 (Template Method Pattern)
一、什么是模板
既然是模板设计思维,那首先要探索一下到底何为模板,搞清楚现实中模板是啥样的,才能进一步把模板抽象化。
一提到模板,我最先想到的是PowerPoint,没错,就是让人头疼的PPT,马上要述职汇报了,PPT确实让人头大~
人事的小姐姐已经把汇报的ppt模板发给我们了,我看了下汇报流程,如下:
不管你是张三、李四还是王五,都要按照这4个方面按顺序进行汇报,不能别出心裁搞个性化,这个就是模板的作用。
百度百科给了“模板”一个定义:“模板是将一个事物的结构规律予以固定化、标准化的成果,它体现的是结构形式的标准化。”
二、现实生活中的模板
勤劳的小伙伴都做过饭吧?一起炒个菜吧!
以上,炒茄子和炒藕带都可以大致分成四步,其中第一和第四步都是一样的,第二步因为炒的菜不同,第三步根据口味放的调料不同,但无论是炒啥菜都基本遵循以上四步,只是个别步骤内容不同,这样我们就可以把上面的步骤抽象成模板,如下:
三、模板的代码实现
上面的模板是现实世界的抽象出来的,如何在代码的世界实现呢?
一般需要一个基类及其子类进行实现,基类一般是一个抽象类,为所有子类提供算法框架;子类则根据自身需求提供具体的实现。上面的可以炒XXX的模板可以理解为抽象基类,炒茄子或炒藕带就是具体子类。
代码实现如下:
抽象基类
/**
* 抽象基类,为所有子类提供算法框架
* (为什么是抽象类而不是接口? 因为抽象类内允许实现一些共有或通用的方法,这些方法就
* 无需每个子类再去实现了,如上面的”准备炒锅“和“装盘”是通用无差别的方法,可以直接在
* 基类实现;而接口内都为抽象方法,不能实现一些通用的方法)
*/
public abstract class Cooking {
/** 炒菜的步骤模板方法 final 方法可以被继承和重载,但不能被重写,模板方法流程是固定的 */
public final void doCooking(){
// 1.准备炒锅
preparePot();
// 2.开始炒菜
startFry();
// 3.放调料
addCondiments();
// 4.装盘
panning();
}
/** 1.准备炒锅(该方法为通用无差别方法,直接在基类里实现,private则子类不可见) */
private void preparePot(){
System.out.println("准备炒锅");
}
/**
* 2.开始炒菜
* (该方法为个性化方法,延迟到子类里实现,定义为抽象方法
* 因此不能用private修饰,用该为protected或default)
*/
protected abstract void startFry();
/**
* 3.放调料
* (该方法为个性化方法,延迟到子类里实现,定义为抽象方法
* 因此不能用private修饰,用该为protected或default)
*/
protected abstract void addCondiments();
/** 4.装盘(该方法为通用无差别方法,直接在基类里实现,private则子类不可见) */
private void panning(){
System.out.println("装盘");
}
}
炒茄子子类
public class Eggplant extends Cooking{
@Override
protected void startFry() {
System.out.println("开始炒茄子");
}
@Override
protected void addCondiments() {
System.out.println("放盐");
}
}
炒藕带子类:
/**
* 子类:炒藕带
*/
public class Lotus extends Cooking{
@Override
protected void startFry() {
System.out.println("开始炒藕带");
}
@Override
protected void addCondiments() {
System.out.println("放醋");
}
}
测试类:
public class Test {
public static void main(String[] args) {
Cooking cookEggplant = new Eggplant();
Cooking cookLotus = new Lotus();
cookEggplant.doCooking();
System.out.println("-----------");
cookLotus.doCooking();
}
}
结果:
准备炒锅
开始炒茄子
放盐
装盘
-----------
准备炒锅
开始炒藕带
放醋
装盘
ps:这里延迟到子类里面实现的方法都是用protect修饰的,为什么?
一方面,这种方法只有子类才能实现,其他外部类不能实现,protect修饰可以满足要求;
另一方面,这里不用public是因为模板类里面的方法组成一个整体流程,给外面的类实现其中一个也没有意义。
四、钩子方法
以上实现了模板方法,子类根据需要继承并实现基类的抽象方法,但是这里是实现全部的抽象方法,如果现在有个子类不想实现“装盘”方法,炒好了就想直接在锅里吃,怎么办呢?
这时候需要提供一个钩子方法,提供一个默认或者空的实现,此钩子方法可以作为子类是否实现基类某个方法的判断依据。
例如我们在基类添加一个钩子方法如下:
/**
* 抽象基类,为为所有子类提供算法框架
* (为什么是抽象类而不是接口? 因为抽象类内允许实现一些共有或通用的方法,这些方法就
* 无需每个子类再去实现了,如上面的”准备炒锅“和“装盘”是通用无差别的方法,可以直接在
* 基类实现;而接口内都为抽象方法,不能实现一些通用的方法)
*/
public abstract class Cooking {
/** 炒菜的步骤模板方法 */
public final void doCooking(){
// 1.准备炒锅
preparePot();
// 2.开始炒菜
startFry();
// 3.放调料
addCondiments();
// 4.装盘
if(willPan()){
panning();
}
}
/** 1.准备炒锅(该方法为通用无差别方法,直接在基类里实现,private则子类不可见) */
private void preparePot(){
System.out.println("准备炒锅");
}
/**
* 2.开始炒菜
* (该方法为个性化方法,延迟到子类里实现,定义为抽象方法
* 因此不能用private修饰,用该为protected或default)
*/
protected abstract void startFry();
/**
* 3.放调料
* (该方法为个性化方法,延迟到子类里实现,定义为抽象方法
* 因此不能用private修饰,用该为protected或default)
*/
protected abstract void addCondiments();
/** 4.装盘(该方法为通用无差别方法,直接在基类里实现,private则子类不可见) */
private void panning(){
System.out.println("装盘");
}
/** 钩子方法 (为另一个方法提供默认或空的实现, protected让子类可以覆写)*/
protected boolean willPan(){
return true;
}
}
子类覆写钩子方法:
/**
* 子类:炒茄子
*/
public class Eggplant extends Cooking{
@Override
protected void startFry() {
System.out.println("开始炒茄子");
}
@Override
protected void addCondiments() {
System.out.println("放盐");
}
/** 覆写基类的钩子方法 (不“装盘”)*/
@Override
protected boolean willPan(){
return false;
}
}
测试类Test:
public class Test {
public static void main(String[] args) {
Cooking cookEggplant = new Eggplant();
Cooking cookLotus = new Lotus();
cookEggplant.doCooking();
System.out.println("-----------");
cookLotus.doCooking();
}
}
结果:
准备炒锅
开始炒茄子
放盐
-----------
准备炒锅
开始炒藕带
放醋
装盘
炒茄子已经不装盘了。可以看出,钩子方法使子类自由度提高了,想不实现哪个流程就不用实现,但总体还是按照模板进行的
五、优缺点
以上过程以不难看出模板设计模式的优点
优点:
- 封装性好
- 复用性好
- 屏蔽细节
- 便于维护
缺点:
由于java的单继承性,一旦一个类采用了模板设计模式,就意味着它需要继承基类,也同时意味着它不能再继承另一个类。
每个不同的实现都需要定义一个子类,会导致类的个数增加。
六、适用的场景
- 算法或操作遵循相似的逻辑或流程
- 把相同的代码抽取到父类中时
- 当算法很复杂时,可以利用模板方法将算法进行分解
七、总结
模板设计的现实需要两个要素:抽象基类和子类
抽象基类中,将部分通用无差别的逻辑以具体方法实现;另外个性化差异的部分声明成抽象方法,在其子类中延迟实现;钩子方法增加子类的灵活性;最后将所有方法汇总成一个finnal修饰的模板方法,调用该方法即可完成整个流程。