八、模板方法模式(Template Method Pattern)《HeadFirst设计模式》读书笔记
在抽象类中创建一个模板方法,这个方法可以调用在抽象类中定义好的其它方法,这些方法可以是抽象的,也可以是默认方法,甚至还可以是一个空的方法(叫做钩子),可以在子类中重写抽象方法或重写钩子方法,从而实现模板方法的某些具体步骤,这就是模板方法模式。模板方法模式可以实现代码的复用。
让我们来看一个具体的例子:
咖啡和柠檬茶的制作过程都要经过四个步骤:
1.烧开水;
2.冲咖啡/泡茶叶;
3.倒入杯中;
4.加糖和牛奶/加柠檬。
可以看到,四个步骤中第1步和第3步其实是一样的,如果把上面的过程分别写进咖啡类和柠檬茶类中的话,那每个类中会有两个方法是重复的,如何实现方法的复用呢?
经过了一些设计模式的学习,我们发现很多设计模式都是通过组合和继承来实现的,这里说说我自己的想法:
1.如果通过组合的方式,可以像命令模式那样把不同的操作步骤封装成一个对象中的方法,然后在咖啡类和柠檬茶类中分别组合这些对象,这样每个步骤都可以被复用;
2.如果通过继承的方式,可以抽象出一个父类,在父类中将相同的步骤1和3写成默认方法,将不同的步骤写成抽象方法,让咖啡和柠檬茶继承这个父类,并分别提供自己对抽象方法的重写。
上面说的第二种方式其实就是模板方法模式的标准形式,来看一下具体代码实现:
public abstract class Father { //模板方法,不希望被改变(被子类覆盖),所以定义成final的 public final void templateMethod(){ boilWater(); brew(); pourIntoCup(); addOther(); } //步骤1 public void boilWater(){ System.out.println("烧开水"); } //步骤2 public abstract void brew(); //步骤3 public void pourIntoCup(){ System.out.println("倒入杯中"); } //步骤4 public abstract void addOther(); } public class Coffee extends Father { //在咖啡类中重写父类中的抽象方法 @Override public void brew() { System.out.println("冲咖啡"); } @Override public void addOther() { System.out.println("加糖和牛奶"); } } public class LemonTea extends Father { //在柠檬茶类中重写父类中的抽象方法 @Override public void brew() { System.out.println("泡茶"); } @Override public void addOther() { System.out.println("加柠檬"); } } //测试 public class test01 { public static void main(String[] args) { Coffee coffee = new Coffee(); LemonTea lemonTea = new LemonTea(); coffee.templateMethod(); lemonTea.templateMethod(); } }
测试结果:
注意到如果不希望模板方法被覆盖,可以声明成final。模板方法定义好了整个方法的框架,有些具体的部分可以在子类中去实现。
另外上面提到的钩子又是怎么一回事呢,我们改造一下上面的例子,在模板方法中加入一个hook()方法:
public abstract class Father { //模板方法,不希望被改变(被子类覆盖),所以定义成final的 public final void templateMethod(){ boilWater(); brew(); pourIntoCup(); addOther(); hook(); } private void hook() { //什么都不做,由子类决定是否覆盖 } //步骤1 public void boilWater(){ System.out.println("烧开水"); } //步骤2 public abstract void brew(); //步骤3 public void pourIntoCup(){ System.out.println("倒入杯中"); } //步骤4 public abstract void addOther(); }
可以看到这个hook()方法并不是抽象方法,也就是子类覆不覆盖都可以,这样可以更灵活的附加一些功能。另外还可以做一些条件判断来控制模板方法内一些方法的执行,比如:
public abstract class Father { //模板方法,不希望被改变(被子类覆盖),所以定义成final的 public final void templateMethod(){ boilWater(); brew(); pourIntoCup(); //是否要加额外的配料 if (wannaAdd()) { addOther(); } } //默认返回true private boolean wannaAdd() { return true; } //步骤1 public void boilWater(){ System.out.println("烧开水"); } //步骤2 public abstract void brew(); //步骤3 public void pourIntoCup(){ System.out.println("倒入杯中"); } //步骤4 public abstract void addOther(); }
上面的案例默认返回true,也就是要加配料,子类可以覆盖这个方法返回false,还可以让用户输入是否要加配料来返回true/false,钩子为模板方法提供了更多的灵活性。
另外上面的例子是模板方法的标准形式,那么不标准的呢,下面说一个实际的例子。在JDK1.8中的Arrays类中的静态方法sort(Object[] o),它可以对数组中的元素进行排序,在sort(Object[] o)方法中调用了一个legacyMergeSort()方法,这个方法内又调用了mergeSort()方法,这是一个模板方法,在这个方法中自己实现了一部分,另外抽象的方法是通过将传递进来的数组元素强转成Comparable接口调用的compareTo()方法,因此数组元素要实现Comparable接口并重写compareTo()方法。可见这个模板方法的实现和上面的标准形式稍微有些不同,因为数组的特殊性,JDK在Arrays工具类写了一些处理数组的方法,比如sort(),在内部的模板方法内,通过数组元素实现Comparable接口并重写compareTo()方法,实现将具体实现延迟到子类(数组元素)中,由这种表述方式也可以知道其实工厂方法也是一种特殊的模板方法,只不过它是用来创建对象的。
总结:
1.模板方法定义了算法的步骤,一些步骤可以在子类中实现,实现了代码的复用,还可以通过钩子实现更灵活的控制;
2.如果不想让子类改变模板方法,可以将模板方法定义成final的
3.模板方法也有一些衍生情况,有时并不一定是在模板方法所在类的子类中去实现方法,比如Arrays工具类,就在模板方法中调用了Comparable接口的抽象方法compareTo(),具体实现是在数组元素中重写的。
4.工厂方法其实也可以看成是模板方法的一种特殊版本,父类中的创建对象的抽象方法就可以看成模板方法,在子类中去返回具体的对象。
5.模板方法的特殊情况其实有点类似于通过组合来实现的,只不过传递进来参数不是接口而是具体实现然后再强转成接口,如果说是组合的话那就有点类似于策略模式,不同的是策略模式通常通过组合实现了算法的全部,而模板方法只是实现了模板中的一部分。