模板方法模式
之前所学习的模式都是围绕着封装进行,如对象创建、方法调用、复杂接口的封装等,这次的模板方法模式将深入封装算法块,好让子类可以在任何时候都将自己挂接进运算里。
模板方法定义:模板方法模式在一个方法中定义一个算法的骨架,而将一些步骤延迟到子类中。模板方法使得子类可以在不改变算法结构的情况下,重新定义算法中的某些步骤。
模板方法就是一个固定步骤的“算法”骨架方法。这个算法的可变部分通过继承,在子类中重载实现。这样就可以在算法骨架不变的情况下,算法细节步骤根据不同的需求进行适应的改变。
例题:茶饮店的饮品冲泡程序(泡茶与泡咖啡)
1 2 3 4 5 6 |
|
我们发现两者的步骤非常相似,仅有部分细节不一:如泡茶冲的是茶叶,加的是蜂蜜;泡咖啡加的是牛奶其实泡茶和泡咖啡的过程就是一个固定骨架步骤的“算法”,我们可以抽象为:
- 煮沸水
- 冲泡
- 根据需求加入调料
- 将泡好的饮料倒入杯子
斜体部分为算法中不一样的部分,如何解决?下面,我们用“模板方法模式”来解决这种不一致。
首先,定义一个含有固定骨架“模板方法”的咖啡因饮料抽象类:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 |
|
这时,准备饮料的四个固定步骤我们都写在模板方法prepareRecipe()里了。这个算法步骤是不可更改的,所以我们给这个模板方法加了final关键字。
然后,根据茶和咖啡在算法步骤上的不同,我们设计两个类,继承抽象方法,分别重载模板方法中的步骤,从而实现茶和咖啡在算法步骤中各自的不同:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
|
测试:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
|
可以看到,通过继承,模板方法在茶和咖啡中的实现有了差别。
模板方法将算法定义成一组步骤,其中的任何步骤都可以是抽象的,由子类负责实现。这样可以确保算法的结构保持不变,同时由子类提供部分的实现。
所以,模板方法就是定义了一个算法的步骤,并允许子类为一个或多个步骤提供实现。
最后,我们给出模板方法的类图:
看上去模板方法似乎就这样结束了,然而,在上面定义的抽象类中还有一个“钩子(hook)”方法,
什么是“钩子”方法呢?我们来看一下定义:
钩子是一种被声明在抽象类中的方法,但只有空的或者默认的实现。钩子的存在,可以让子类有能力对算法的不同点进行挂钩。要不要挂钩,由子类自行决定。
在上面的代码中,我们写了一个钩子来决定是否加调料
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
|
现在,我们用一个子类来挂钩:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 |
|
这时,我们准备茶水时就能根据顾客的回答而安排需要加调料这一步骤了。子类通过覆盖钩子方法,实现了算法中的可选部分。
模板方法模式中还使用到了一个新的设计原则:好莱坞原则
好莱坞原则:别调用(打电话给)我们,我们会调用(打电话给)你
好莱坞原则可以给我们一种防止“依赖腐败”的方法。当高层组件依赖低层组件,而低层组件又依赖高层组件,而高层组件又依赖边侧组件,而边侧组件又依赖低层组件时,依赖腐败就发生了。在这种情况下,没有人可以轻易地搞懂系统是如何设计的。
在好莱坞原则之下,我们允许低层组件将自己挂钩到系统上,但是高层组件会决定什么时候和怎样使用这些低层组件。换句话说,高层组件对待低层组件的方式是“别调用我们,我们会调用你”。
好莱坞原则就是确保不会出现高层组件依赖底层组件、底层组件又依赖高层组件的“依赖腐败”。只有高层组件会决定什么时候和怎样使用底层组件,而底层组件不会调用高层组件。
在模板方法模式中,算法的实现会调用到具体子类的某个方法,也就是高层组件依赖于底层组件。具体子类不会调用父类中的方法,不会形成底层组件依赖高层组件的环状依赖:
采用好莱坞原则的设计模式还有:工厂方法(可以看作特殊的模板方法),观察者、装饰者...
好莱坞原则和依赖倒置原则的关系:依赖倒置原则是尽量避免使用具体类,多使用抽象。
策略模式与模板方法模式
策略模式和模板方法模式很像,都是针对算法改变的情况的设计模式。以下是它们的区别:
- 策略模式是采用的组合来实现算法的变化,这样的设计更加灵活,依赖性程度低;
- 模板方法模式采用的继承来实现算法中的变化部分,这样的设计对算法有更多的控制权,且代码的重复会少一些,但由于算法依赖于父类,所以依赖程度高。
Java API中的模板方法
Java中较常见的模板方法模式的应用:
- java.io的InputStream类有一个read()方法,是由子类实现的,而这个方法又会被read(byte b[], int off, int len)模板方法使用。
- Swing的JFrame继承了一个paint()方法。在默认状态下,paint()是不做事情的,因为它是一个“钩子”。通过覆盖paint(),可以将自己的代码插入JFrame的算法中,显示出想要的画面。
- applet是一个能在网页上面执行的小程序。任何applet必须继承自Applet类,而Applet类中提供了好些钩子。
- Java数组排序方法,如Arrays.sort(Object[]);Object对象实现了Comparable接口,排序通过Comparable接口中的compareTo()实现。
注意,这个排序的例子表面看上去好像与模板方法模式无关(没有用到继承),但实质仍是通过子类提供算法步骤的实现来实现了算法的变化。虽然采用了组合,但思想仍是模板方法模式的思想。
总结
模板方法模式:在一个方法中定义一个算法的骨架,而将一些步骤延迟到子类中。模板方法使得子类可以在不改变算法的结构情况下,重新定义算法中的某些步骤。
- 模板方法定义了算法的步骤,把这些步骤实现延迟到子类。
- 模板方法为我们提供了一种代码复用的重要技巧。
- 模板方法的抽象类可以定义具体的方法、抽象方法和钩子方法。
- 抽象方法由子类实现。
- 钩子是一种方法,它在抽象类中不做事,或者只做默认的事,子类可以选择要不要覆盖它。
- 好莱坞原则告诉我们将决策权放在高层模板中,以便决定如何及何时调用底层模块。
- 策略模式和模板方法模式都封装算法,一个是用组合,一个是用继承。
- 工厂方法是模板方法的一种特殊版本