定义
在模板方法中定义一个算法的骨架,而将部分步骤延迟到子类中,使子类在不改变算法结构的情况下,重新定义算法中的部分步骤。
理解该模式的关键就在“模板”二字。什么是模板?模板就是预先定好了结构,后来使用者照葫芦画瓢即可。
通常情况下,算法指用于处理某类任务的确切的指令序列,是对处理逻辑的封装。模板方法模式定义中的骨架同样是对处理逻辑的封装,只是算法中封装的是具体的处理逻辑,即每一步的具体操作。模板方法则封装一个大的步骤,每个步骤中包括若干数量的具体操作,它的封装粒度比算法定义的粒度要稍大。
模式结构
该模式包含模板定义者和模板使用者两类。在java实现中,两者存在继承关系,模板定义者为基类,模板使用者为子类。
模板基类
该类被定义为抽象类,包括模板方法、共有方法、钩子方法、抽象方法。其中
- 不变的共有方法
用于封装全部子类共有的处理逻辑,各个子类稳定的部分。
在模板基类中,这种方法的处理逻辑已被实现,被final修饰,子类不可修改。 - 可变的抽象方法
用于封装各个子类特有的处理逻辑。
在模板基类中,这种方法被定义为抽象方法,被abstract
修饰,由子类实现具体的方法逻辑。这类方法也是模板基类被定义为抽象类的原因。
- 钩子方法
是模板对子类更进一步的开放和扩展,用于控制模板方法是否执行某些步骤,一般返回ture
或false
。通过判断钩子方法的返回值,来确定是否执行某些步骤。钩子方法是子类和父类通信的通道。
在模板基类中,这种方法被定义为一般方法,可用protected
修饰,子类可以重写。通俗地,钩子可分为两部分,拿钩子的一方和被钩的一方。此处拿钩子的是子类 ,被勾的是对可选方法的调用开关。子类使用钩子钩住调用开关,往左钩是调用,往右钩是不调用。
- 模板方法
模板方法模式的核心是模板方法,模板方法实际上是对该模式定义中骨架的实现。模板方法中有序调用若干方法,每一个方法表示一个处理步骤,全部的处理步骤构成一个处理特定业务问题的处理逻辑。
其中,模板方法内部调用的方法包括上面介绍的三种。需要说明的时,模板方法对以上处理方法的调用顺序是固定的,具备业务含义,是对规范(即骨架)的实现,因此子类不能修改其调用顺序。
在模板基类中,这种方法的处理逻辑已被实现,被final修饰,子类不可修改。
模板子类
子类继承模板基类,根据业务场景需要,有选择地实现基类中定义的可变方法和钩子方法。
类图
![](https://i-blog.csdnimg.cn/blog_migrate/1b16db92cb192b2b69597082624e42a2.png)
优缺点
- 优点
- 模板方法可以让算法的细节掩盖在子类,同时抽取公共的算法,提高代码复用程度
- 模板方法可以让修改控制在子类,而父类方法不需要进行改动,符合开放关闭原则。
- 缺点
- 类数目增加
- 继承产生的问题。模板方法类的改动对于所有的算法实现子类都会产生影响,同时模板父类改动违背“开放-关闭”原则
- 模板方法由于利用钩子控制父类方法,会导致反向控制代码,对于代码的阅读不是十分友好。
代码实践
该业务以制作网课为例,制作网课的流程包含四个步骤
- 制作ppt;
- 制作视频;
- 做笔记 可选项,有些课程做笔记,有些不做;
- 提供素材,可变项,不同课程的素材不同;
模板基类
/**
* @description: 抽象课程
* @Date: 2021/11/24 11:09
*/
public abstract class ACourse {
//模板方法
//规范了处理业务的模板,模板包括
// 1)不变的公共方法,指各个子类都必须执行的、稳定的部分,被final修饰
// 2)可选方法,指各子类可选的部分,子类使用钩子方法确定是否在模板中使用这一类方法。
// 3)变化的方法,指各子类各不相同的部分,子类根据自身业务实现处理逻辑,被abstract修饰。
final void makeCourse(){
//不变的方法1-1
makePPT();
//不变的方法1-2
makeVideo();
if(needWriteNote()){
//可选方法
makeNote();
}
//可变的方法
packageCourse();
}
//所有子类的公共操作,子类不可修改其处理逻辑。即关闭对 不变的部分 的修改操作
//但子类可以基于钩子方法决定是否使用该方法
final void makePPT(){
System.out.println("制作ppt");
}
//所有子类的公共操作,子类不可修改其处理逻辑。即关闭对 不变的部分 的修改操作
//但子类可以基于钩子方法决定是否使用该方法
final void makeVideo(){
System.out.println("制作视频");
}
//所有子类的公共操作,子类不可修改其处理逻辑。即关闭对 不变的部分 的修改操作
//但子类可以基于钩子方法决定是否使用该方法
final void makeNote(){
System.out.println("编写笔记");
}
/**
* 钩子方法,用户可以重写,进而决定基类定义的骨架中是否执行某些方法
* 此处的钩子方法用于控制 是否在模板中执行makeNote方法
* @return
*/
protected boolean needWriteNote(){
return false;
}
//基类中变化的部分由子类实现,即对扩展开放
abstract void packageCourse();
}
三种模板子类
-
前端课程
不做笔记,即使用默认钩子实现。/** * @description: 前端课程 是ACourse的子类 * @Date: 2021/11/24 14:19 */ public class FrontCourse extends ACourse{ @Override void packageCourse() { //前端课程类的处理逻辑 System.out.println("提供前端课程素材"); } }
-
设计模式课程
需要笔记,则需要重写钩子方法/** * @description: 设计模式课程 是ACourse的子类 * @Date: 2021/11/24 14:18 */ public class DesignPatternCourse extends ACourse{ @Override void packageCourse() { //设计模式课程类的处理逻辑 System.out.println("提供设计模式源码"); } /** * 重写钩子方法,实现对骨架的控制 * 此处指在骨架中使用 makeNote方法 * @return */ @Override protected boolean needWriteNote() { return true; } }
-
运动课程
在某些情况下,子类无法提前确定模板方法是否调用可选方法,即无法确定钩子方法成立的条件。此时需要将判断条件交给下一层(相对于子类的下一层,即调用子类的位置)调用者。/** * @description: 运动课程 Acoure子类 * @Date: 2021/11/24 15:09 */ public class SprotCourse extends ACourse { private boolean needMakeNote = false; @Override void packageCourse() { System.out.println("体育课"); } /** * 构造器是调用者同本类通信的窗口,似乎也可称之为钩子方法。 * @param needMakeNote */ public SprotCourse(boolean needMakeNote) { this.needMakeNote = needMakeNote; } /** * 由于业务拆分不内聚,基类的子类无法确定使钩子函数成立的条件 * 则将该条件下方给下一层。此处是运动类课无法确定是否做笔记,则将决定权交给下一级,即调用者。 * 譬如运动课分为电子竞技运动和球类运动,电子竞技不做笔记,但球类却需要做笔记,无法直接在运动类中确定做不做笔记。 * @return */ @Override protected boolean needWriteNote() { return this.needMakeNote; } }
适用场景&模式识别
- 适用场景
- 一次性实现一个算法的不变部分,并将可变的行为留给子类来实现
- 各子类中公共的行为被提取出来集中到一个公共父类中,避免代码冗余。
- 模式识别
如何识别是否是模板方法 、钩子方法、子类要实现的方法 ,可尝试使用以下三种方式。- 观察类图,通过继承关系判断 且父类中有些已实现 一般使用abstract修饰;
- 钩子方法多用于判断;
- 父类中定义为抽象方法的方法 需要子类实现
源码应用
- list arraylist
- mybatis 中baseExecutor Executor方法
相关知识
- 抽象类
抽象类不能用来实例化对象,声明抽象类的唯一目的是为了将来对该类进行扩充。
如果一个类继承于一个抽象类,则子类必须实现父类的抽象方法。如果子类没有实现父类的抽象方法,则必须将子类也定义为abstract类。 - 访问修饰符
- private 和 public 两极分化,前者只能在类内部访问,后者在所有位置可访问
- default 包访问权限,只要在一个包下都可以访问,也是默认访问修饰符
- protected 包和子类可以访问,有特殊情况,略去。
- 通过重写钩子方法可知,子类重写父类的方法M,那么父类在调用M时,会优先调用子类重写后的M
- 优点:通过继承相同的父类,初始化子类时,父类会调用不同子类的不同复写方法,从而实现多态性。
- 缺点:如果在父类构造函数中调用被子类重写的方法,会导致子类重写的方法在子类构造器的所有代码之前执行,从而导致子类重写的方法访问不到子类实例变量的值,因为此时这些变量还没有被初始化。
参考资料
- Head First 设计模式(中文版)
- 浅谈设计模式 - 模板方法(十)