前言在面向对象程序设计中继承常被用来复用代码。但如何优雅地定义算法的通用步骤同时允许子类填充具体细节这正是模板方法模式Template Method Pattern的用武之地。它将算法的骨架定义在父类中将可变的部分延迟到子类实现从而实现代码复用与扩展性的完美平衡。本文将通过经典的“咖啡与茶”案例深入讲解模板方法模式的核心思想并探讨在 JavaScript 这一动态语言中的实现方式与替代方案。一、模板方法模式的定义与组成模板方法模式是一种基于继承的设计模式其核心结构包含两部分抽象父类封装了子类的算法框架包括公共方法以及各方法的执行顺序。其中定义了模板方法它规定了算法的执行步骤。具体子类继承抽象父类重写父类中某些抽象方法提供具体的实现。这种模式将不变的行为搬移到父类去除了子类中的重复代码同时让子类可以灵活地扩展变化的部分。二、第一个例子Coffee or Tea2.1 先泡一杯咖啡泡咖啡的步骤通常如下把水煮沸用沸水冲泡咖啡把咖啡倒进杯子加糖和牛奶代码实现var Coffee function () {}; Coffee.prototype.boilWater function () { console.log(把水煮沸); }; Coffee.prototype.brewCoffeeGriends function () { console.log(用沸水冲泡咖啡); }; Coffee.prototype.pourInCup function () { console.log(把咖啡倒进杯子); }; Coffee.prototype.addSugarAndMilk function () { console.log(加糖和牛奶); }; Coffee.prototype.init function () { this.boilWater(); this.brewCoffeeGriends(); this.pourInCup(); this.addSugarAndMilk(); }; var coffee new Coffee(); coffee.init();2.2 泡一壶茶泡茶的步骤类似把水煮沸用沸水浸泡茶叶把茶水倒进杯子加柠檬代码实现var Tea function () {}; Tea.prototype.boilWater function () { console.log(把水煮沸); }; Tea.prototype.steepeTaBag function () { console.log(用沸水浸泡茶叶); }; Tea.prototype.pourInCup function () { console.log(把茶水倒进杯子); }; Tea.prototype.addLemon function () { console.log(加柠檬); }; Tea.prototype.init function () { this.boilWater(); this.steepeTaBag(); this.pourInCup(); this.addLemon(); }; var tea new Tea(); tea.init();2.3 分离出共同点观察咖啡和茶的冲泡过程发现它们可以抽象为四个步骤把水煮沸用沸水冲泡饮料把饮料倒进杯子加调料于是我们可以创建一个抽象父类Beverage封装算法骨架var Beverage function () {}; Beverage.prototype.boilWater function () { console.log(把水煮沸); }; Beverage.prototype.brew function () {}; // 空方法由子类重写 Beverage.prototype.pourInCup function () {}; // 空方法由子类重写 Beverage.prototype.addCondiments function () {}; // 空方法由子类重写 Beverage.prototype.init function () { this.boilWater(); this.brew(); this.pourInCup(); this.addCondiments(); };2.4 创建 Coffee 和 Tea 子类var Coffee function () {}; Coffee.prototype new Beverage(); Coffee.prototype.brew function () { console.log(用沸水冲泡咖啡); }; Coffee.prototype.pourInCup function () { console.log(把咖啡倒进杯子); }; Coffee.prototype.addCondiments function () { console.log(加糖和牛奶); }; var coffee new Coffee(); coffee.init(); // 输出顺序把水煮沸 → 用沸水冲泡咖啡 → 把咖啡倒进杯子 → 加糖和牛奶接着创建Tea子类var Tea function () {}; Tea.prototype new Beverage(); Tea.prototype.brew function () { console.log(用沸水浸泡茶叶); }; Tea.prototype.pourInCup function () { console.log(把茶倒进杯子); }; Tea.prototype.addCondiments function () { console.log(加柠檬); }; var tea new Tea(); tea.init(); // 输出顺序把水煮沸 → 用沸水浸泡茶叶 → 把茶倒进杯子 → 加柠檬这里的Beverage.prototype.init就是模板方法它规定了子类必须按照这个顺序执行步骤。子类只需重写brew、pourInCup、addCondiments等方法即可。三、抽象类在模板方法模式中的作用3.1 Java 中的抽象类在 Java 等静态语言中模板方法模式通常依赖抽象类。抽象类不能被实例化它用于定义契约并强制子类实现抽象方法。例如public abstract class Beverage { final void init() { // 模板方法 boilWater(); brew(); pourInCup(); addCondiments(); } void boilWater() { System.out.println(把水煮沸); } abstract void brew(); abstract void pourInCup(); abstract void addCondiments(); } public class Coffee extends Beverage { void brew() { System.out.println(用沸水冲泡咖啡); } void pourInCup() { System.out.println(把咖啡倒进杯子); } void addCondiments() { System.out.println(加糖和牛奶); } }抽象类保证了子类必须实现所有抽象方法否则编译报错。这种机制有效地防止了遗漏实现。3.2 JavaScript 中没有抽象类的变通JavaScript 没有抽象类的语法但我们可以通过以下方式模拟方案一让父类方法抛出异常Beverage.prototype.brew function () { throw new Error(子类必须重写 brew 方法); }; Beverage.prototype.pourInCup function () { throw new Error(子类必须重写 pourInCup 方法); }; Beverage.prototype.addCondiments function () { throw new Error(子类必须重写 addCondiments 方法); };这样如果子类忘记实现某个方法运行时会抛出错误虽然检查时机较晚但至少能提醒开发者。方案二使用鸭子类型模拟接口检查较复杂不推荐四、钩子方法为算法骨架添加“开关”有时子类可能不需要执行父类算法中的某一步例如某些人喝咖啡不加糖。钩子方法Hook正是为此而生——它在父类中提供一个默认的空实现或返回true子类可以选择性地重写它来控制算法的走向。Beverage.prototype.customerWantsCondiments function () { return true; // 默认需要调料 }; Beverage.prototype.init function () { this.boilWater(); this.brew(); this.pourInCup(); if (this.customerWantsCondiments()) { this.addCondiments(); } }; // 子类重写钩子方法 var 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(请问需要调料吗); }; var coffee new CoffeeWithHook(); coffee.init(); // 根据用户选择决定是否加糖和牛奶钩子方法让模板方法更加灵活将更多的控制权交给子类。五、好莱坞原则别调用我们我们会调用你好莱坞原则Hollywood Principle是模板方法模式的指导思想高层组件决定何时调用底层组件底层组件不应主动调用高层组件。通俗地说就是“别打电话给我们我们会打电话给你”。在模板方法模式中父类高层定义了算法框架并调用子类底层的方法而子类只需提供具体实现从不主动调用父类的方法。这种控制反转的设计使得代码结构清晰耦合度降低。好莱坞原则也体现在发布-订阅模式、回调函数等场景中。六、真的需要“继承”吗——高阶函数替代方案JavaScript 是动态语言我们不一定非要通过继承来实现模板方法。利用高阶函数可以更轻量地达到类似效果。var Beverage function (param) { var boilWater function () { console.log(把水煮沸); }; var brew param.brew || function () { throw new Error(必须传递 brew 方法); }; var pourInCup param.pourInCup || function () { throw new Error(必须传递 pourInCup 方法); }; var addCondiments param.addCondiments || function () { throw new Error(必须传递 addCondiments 方法); }; var F function () {}; F.prototype.init function () { boilWater(); brew(); pourInCup(); addCondiments(); }; return F; }; var Coffee Beverage({ brew: function () { console.log(用沸水冲泡咖啡); }, pourInCup: function () { console.log(把咖啡倒进杯子); }, addCondiments: function () { console.log(加糖和牛奶); } }); var coffee new Coffee(); coffee.init(); // 输出与之前相同这种实现方式避免了手动设置原型链将可变逻辑通过参数传入更加符合 JavaScript 的函数式风格。它依然是模板方法模式的思想但代码更加简洁。七、小结模板方法模式是一种基于继承的经典设计模式它通过将算法骨架封装在父类中把可变部分留给子类实现达到了代码复用和扩展的目的。在 JavaScript 中虽然没有原生的抽象类支持但我们可以通过抛出异常或利用高阶函数来实现类似效果。优点封装不变部分复用代码易于扩展符合开放-封闭原则。缺点对继承的依赖可能导致子类数量膨胀且 JavaScript 中缺少编译时检查。在实际开发中模板方法模式常用于搭建框架的骨架如生命周期钩子、模板渲染等场景。而高阶函数版本则更贴合 JavaScript 的函数式特性让代码更加灵活简洁。