1 定义:
模板方法模式(Template Method)
Define the skeleton of an algorithm in anoperation, deferring some steps to subclasses. Template Method lets subclasses redefine certain steps of an algorithm without changing the algorithm’s structure.(定义一个操作中的算法的框架,而将一些步骤延迟到子类中。使用子类可以不改变一个算法的结构即可重定义该算法的某些特定步骤。)
模板方法模式是一种基于继承的代码复用技术,它是一种类行为型模式。
模板方法模式是结构最简单的行为型设计模式,在其结构中只存在父类与子类之间的继承关系。通过使用模板方法模式,可以将一些复杂流程的实现步骤封装在一系列基本方法中,在抽象父类中提供一个称之为模板方法的方法来定义这些基本方法的执行次序,而通过其子类来覆盖某些步骤,从而使得相同的算法框架可以有不同的执行结果。模板方法模式提供了一个模板方法来定义算法框架,而某些具体步骤的实现可以在其子类中完成。
1.1 通用类图:
由图2可知,模版方法模式由一个抽象类和一个(或一组)实现类通过继承结构组成,模板方法模式包含如下两个角色:
(1)抽象类(AbstractClass):在抽象类中定义了一系列基本操作(Primitive Operations),这些基本操作可以是具体的,也可以是抽象的,每一个基本操作对应算法的一个步骤,在其子类中可以重定义或实现这些步骤。同时,在抽象类中实现了一个模板方法(Template Method),用于定义一个算法的框架,模板方法不仅可以调用在抽象类中实现的基本方法,也可以调用在抽象类的子类中实现的基本方法,还可以调用其他对象中的方法。
抽象类中的方法分为2种:
模版方法:由抽象类声明并加以实现。一般来说,模版方法调用抽象方法来完成主要的逻辑功能,并且,模版方法大多会定义为final类型,指明主要的逻辑功能在子类中不能被重写。由于模板方法是具体方法,因此模板方法模式中的抽象层只能是抽象类,而不是接口。
注意:为了防止恶意的操作,一般模板方法都加上final关键字,不允许被覆写。
基本方法:是实现算法各个步骤的方法,是模板方法的组成部分。基本方法又可以分为三种:抽象方法(Abstract Method)、具体方法(Concrete Method)和钩子方法(Hook Method)。
抽象方法:父类中只声明但不加以实现,而是定义好规范,然后由它的子类去实现。
具体方法:一个具体方法由一个抽象类或具体类声明并实现,其子类可以进行覆盖也可以直接继承。
钩子方法:由抽象类声明并加以实现。但是子类可以去扩展,子类可以通过扩展钩子方法来影响模版方法的逻辑。
(2)具体子类(ConcreteClass):它是抽象类的子类,用于实现在父类中声明的抽象基本操作以完成子类特定算法的步骤,也可以覆盖在父类中已经实现的具体基本操作。
1.2 通用代码:
源码实现如下:
public abstract class AbstractClass {
//基本方法
protected abstract void doSomething();
//模板方法
public final void templateMethod(){
doSomething();
}
}
public class ConcreteClass extends AbstractClass{
@Override
protected void doSomething() {
}
}
public class Client {
public static void main(String[] args){
AbstractClass c = new ConcreteClass();
c.templateMethod();
}
}
2 优点
2.1 封装不变部分,扩展可变部分(可扩展性)
把认为不变部分的算法封装到父类实现,而可变部分的则可以通过继承来继续扩展;
2.2 提取公共部分代码,便于维护(可维护性)
不要让维护人员到处查找相似的代码;
2.3 行为由父类控制,子类实现
基本方法是由子类实现的,因此子类可以通过扩展的方式增加相应的功能,符合开闭原则。
3 缺点
按设计习惯,抽象类负责声明最抽象、最一般的事物属性与方法,实现类完成具体事物属性和方法。但模板方法却反了:抽象类定义了部分抽象方法,由子类来实现,子类执行的结果影响了父类的结果,也就是子类对父类产生了影响,在复杂项目中,会带来代码阅读的困难。对新手产生不适。
4 应用场景
4.1 多个子类有公有的方法,并且逻辑基本相同时。
4.2 重要、复杂的算法,可以把核心算法设计为模板方法,周边的相关细节功能则由各个子类实现。
4.3 重构时,模板方法模式是一个经常使用的模式,把相同的代码抽取到父类中,然后通过钩子函数来约束其行为。
5 注意事项
5.1 抽象模板中的基本方法尽量设计为protected类型,符合迪米特法则,不需要暴露的属性或方法尽量不要设置为protected类型。实现类若非必要,尽量不要扩大父类中的访问权限。
5.2 此也是一种较好的父类调用子类方法的方式。
6 扩展
6.1模板方法模式的本质
模板方法模式的本质:固定算法骨架。
模板方法模式主要是通过制定模板,把算法步骤固定下来,至于谁来实现,模板可以自己实现,也可以由子类去实现,还可以通过回调机制让其它类来实现。
通过固定算法骨架来约束子类的行为,并在特定的扩展点来让子类进行功能扩展,从而让程序既有很好的复用性,又有较好的扩展性。
2、对设计原则的体现
模板方法很好的体现了开闭原则和里氏替换原则。
首先从设计上分离变与不变,然后把不变的部分抽取出来,定义到父类中,比如算法骨架,一些公共的、固定的实现等。这些不变的部分被封闭起来,尽量不去修改它们,要想扩展新的功能,那就使用子类来扩展,通过子类来实现可变化的步骤,对于这种新增功能的做法是开放。
其次,能够实现统一的算法骨架,通过切换不同的具体实现来切换不同的功能,一个根本原因就是里氏替换原则,遵循这个原则,保证所有的子类实现的是同一个算法模板,并能在使用模板的地方,根据需要切换不同的具体实现。
7 范例
7.1泡茶与泡咖啡
我们可以看出泡茶和泡咖啡可以共用一个相同的泡法(算法):
把水煮沸——>用沸水冲泡——>倒入杯子中——>加入调料。
首先是抽象类,该抽象类提供了冲泡咖啡或者茶的具体流程,并且实现了逻辑步骤,煮沸水和倒入杯子中。将用沸水冲泡和加入调料交由具体的子类(咖啡、茶)来实现。
然后是具体的子类实现:
Coffee.java
Tea.java
完成,做了这么久终于可以泡杯咖啡来喝了。
从上面的运行结果可以看出,我们的模板方法模式表现的非常良好,但是我们似乎忽略了一些东西?如果某些客户并不喜欢加入调料,而喜欢原生态的,但是我们的算法总是会给客户加入调料,怎么解决?
遇到这个问题我们可以使用钩子。所谓钩子就是一种被声明在抽象类中的方法,但只有空的或者默认的实现。钩子的存在可以使子类能够对算法的不同点进行挂钩,即让子类能够对模板方法中某些即将发生变化的步骤做出相应的反应。当然要不要挂钩,由子类决定。
所以对于上面的要求,我们可以做出如下修改。
客户是否需要加入调料,只需要回答y或者n
测试程序
运行结果
从上面可以看出钩子能够作为条件来进行控制。
7.2(手机开机过程,有钩子实现)
实现如下:
- package _04_TemplateMethod;
- public abstract class Phone {
- //模板方法:开机
- public final void turnOn(){
- initSystem();
- showLogo();
- if(isPlayMusic())
- playMusic();
- lockAndSleep();
- }
- //初始系统
- protected abstract void initSystem();
- //显示开机logo
- protected abstract void showLogo();
- //播放开机音乐
- protected abstract void playMusic();
- //锁屏并待机
- protected abstract void lockAndSleep();
- //是否播放开机音乐 :钩子函数
- protected boolean isPlayMusic(){
- return true;
- }
- }
- public class SmartPhone extends Phone{
- @Override
- protected void initSystem() {
- System.out.println("正在加载智能机系统 。。。");
- }
- @Override
- protected void showLogo() {
- System.out.println("显示LOGO:3G梦想!");
- }
- @Override
- protected void playMusic() {
- System.out.println("播放开机音乐:123456");
- }
- @Override
- protected void lockAndSleep() {
- System.out.println("一切就绪,锁屏待机");
- }
- @Override
- protected boolean isPlayMusic(){
- return false;//什么年代了,智能机不播开机音乐!
- }
- }
- public class FeaturePhone extends Phone{
- @Override
- protected void initSystem() {
- System.out.println("正在加载功能机系统 。。。");
- }
- @Override
- protected void showLogo() {
- System.out.println("显示LOGO:NOKIA待机王!");
- }
- @Override
- protected void playMusic() {
- System.out.println("播放开机音乐:123456");
- }
- @Override
- protected void lockAndSleep() {
- System.out.println("一切就绪,锁屏待机");
- }
- }
- public class TestPhone {
- public static void main(String[] args) {
- Phone sp = new SmartPhone();
- sp.turnOn();
- Phone fp = new FeaturePhone();
- fp.turnOn();
- }
- }
- 结果:
- 正在加载智能机系统 。。。
- 显示LOGO:3G梦想!
- 一切就绪,锁屏待机
- 正在加载功能机系统 。。。
- 显示LOGO:NOKIA待机王!
- 播放开机音乐:123456
- 一切就绪,锁屏待机