模板方法模式
定义一个抽象类,将部分逻辑(公共逻辑)用具体方法以及具体构造函数的形式实现,然后声明一些抽象方法来迫使子类实现剩余的逻辑。
不同的子类可以以不同的方式实现这些抽象方法,从而对剩余的逻辑有不同的实现。在父类中 封装了子类的算法框架 ,它作为一个算法的模板, 指导子类以何种顺序去执行哪些方法 。
使用场景
比如我们在构建一系列的 UI 组件,这些组件的构建过程一般如下所示:
- 初始化一个 div 容器;
- 通过 ajax 请求拉取相应的数据;
- 把数据渲染到 div 容器里面,完成组件的构造;
- 通知用户组件渲染完毕。
我们看到,任何组件的构建都遵循上面的 4 步,其中第(1)步和第(4)步是相同的。第(2)步不同的地方只是请求 ajax 的远程地址,第(3)步不同的地方是渲染数据的方式。
于是我们可以把这 4 个步骤都抽象到父类的模板方法里面,父类中还可以顺便提供第(1)步和第(4)步的具体实现。当子类继承这个父类之后,会重写模板方法里面的第(2)步和第(3)步。
Java实现
在像Java,C++等静态类型语言中,模板方法模式的实现非常依赖抽象类,通过抽象类定义模板方法(方法的执行顺序),以及一些公共方法的实现。子类通过继承抽象类实现剩余的方法。
抽象类的模板方法也会帮我们检测子类对于剩余逻辑的实现情况(如果子类没有实现,编译器就会报错)。
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 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77
| public abstract class Beverage {
final void init() { boilWater(); brew(); pourInCup(); addCondiments(); }
void boilWater() { System.out.println("把水煮沸"); }
abstract void brew();
abstract void addCondiments();
abstract void pourInCup(); }
public class Coffee extends Beverage {
@Override void brew() { System.out.println("用沸水冲泡咖啡"); }
@Override void pourInCup() { System.out.println("把咖啡倒进杯子"); }
@Override void addCondiments() { System.out.println("加糖和牛奶"); } }
public class Tea extends Beverage {
@Override void brew() { System.out.println("用沸水浸泡茶叶"); }
@Override void pourInCup() { System.out.println("把茶倒进杯子"); }
@Override void addCondiments() { System.out.println("加柠檬"); } }
public class Test {
private static void prepareRecipe(Beverage beverage) { beverage.init(); }
public static void main(String args[]) { Beverage coffee = new Coffee(); prepareRecipe(coffee); Beverage tea = new Tea(); prepareRecipe(tea); } }
|
Javascript实现
同时如果我们不想加糖,我们可以定义一个公共方法( 称为钩子方法 ),在模板方法中 通过钩子方法控制是否执行某个逻辑 。
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
| const Beverage = function () { }; Beverage.prototype.boilWater = function () { console.log('把水煮沸'); }; Beverage.prototype.brew = function () { throw new Error('子类必须重写 brew 方法'); }; Beverage.prototype.pourInCup = function () { throw new Error('子类必须重写 pourInCup 方法'); }; Beverage.prototype.addCondiments = function () { throw new Error('子类必须重写 addCondiments 方法'); }; Beverage.prototype.customerWantsCondiments = function () { return true; }; Beverage.prototype.init = function () { this.boilWater(); this.brew(); this.pourInCup(); if (this.customerWantsCondiments()) { this.addCondiments(); } }; const CoffeeWithHook = function () { }; CoffeeWithHook.prototype = new Beverage(); CoffeeWithHook.prototype.brew = function () { console.log('用沸水冲泡咖啡'); }; CoffeeWithHook.prototype.pourInCup = function () { console.log('把咖啡倒进杯子'); }; CoffeeWithHook.prototype.addCondiments = function () { console.log('加糖和牛奶'); }; CoffeeWithHook.prototype.customerWantsCondiments = function () { return window.confirm('请问需要调料吗?'); }; const coffeeWithHook = new CoffeeWithHook(); coffeeWithHook.init();
|
用JavaScript模拟抽象类和子类来并不是一个很好的选择,在Javascript中我们还可以使用高阶函数的形式,将需要在子类中实现的细节通过参数的形式传入。
把 brew、pourInCup、addCondiments 这些方法依次传入 Beverage 函数,Beverage 函数被调用之后返回构造器 F。F 类中包含了“模板方法”F.prototype.init。跟继承得到的效果一样,该“模板方法”里依然封装了饮料子类的算法框架。
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 48
| const Beverage = function (param) { const boilWater = function () { console.log('把水煮沸'); }; const brew = param.brew || function () { throw new Error('必须传递 brew 方法'); }; const pourInCup = param.pourInCup || function () { throw new Error('必须传递 pourInCup 方法'); }; const addCondiments = param.addCondiments || function () { throw new Error('必须传递 addCondiments 方法'); }; const F = function () { }; F.prototype.init = function () { boilWater(); brew(); pourInCup(); addCondiments(); }; return F; }; const Coffee = Beverage({ brew: function () { console.log('用沸水冲泡咖啡'); }, pourInCup: function () { console.log('把咖啡倒进杯子'); }, addCondiments: function () { console.log('加糖和牛奶'); } }); const Tea = Beverage({ brew: function () { console.log('用沸水浸泡茶叶'); }, pourInCup: function () { console.log('把茶倒进杯子'); }, addCondiments: function () { console.log('加柠檬'); } }); const coffee = new Coffee(); coffee.init(); const tea = new Tea(); tea.init();
|
好莱坞原则
允许底层组件将自己挂钩到高层组件中,而高层组件会决定什么时候、以何种方式去使用这些底层组件。
模板方法模式是好莱坞原则的一个典型使用场景,当我们用模板方法模式编写一个程序时,就意味着子类放弃了对自己的控制权,而是改为父类通知子类,哪些方法应该在什么时候被调用。作为子类,只负责提供一些设计上的细节。