Skip to content

模板方法模式(Template Method)

浅谈

模板方法模式是一种使用继承来实现的模式

模板方法模式有两部分组成,第一部分是抽象父类,第二部分是具体的实现子类。通常在抽象父类中封装了子类的算法框架,包括实现一些公共方法以及封装子类中所有方法的执行顺序。子类通过继承这个抽象类,也继承了整个算法结构,并且可以选择重写父类的方法。

使用场景

  • UI组件
  • 框架(如vue中生命周期执行顺序,我们可以重写方法的实现过程,但顺序规定好了)
  • 规定了执行顺序的模板

注意

模板方法模式极度依赖抽象类的,而 JavaScript 却没有抽象类的概念,只能使用原型继承来模拟类似继承,或者选择更好的实现方法——高阶函数。学过 Java 的应该知道什么叫抽象类。

Java 的类分为两类,一种是具体类,一种是抽象类。具体类可以被实例化,而抽象类不能被实例化。比如:饮料为一个抽象类,总不能去跟老板说来瓶饮料吧,老板怎么知道你要的是什么。

抽象类声明抽象方法(为它的子类定义公共接口),但抽象方法没有具体的实现过程。当子类继承了这个抽象类时,必须重写父类的抽象方法。

如果想要详细了解抽象这一概念,可以上网百度即可。


例:咖啡与茶

泡一杯咖啡步骤:

  1. 把水煮沸
  2. 用沸水冲咖啡
  3. 把咖啡倒进杯子
  4. 加糖和牛奶
javascript
const 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()
}

const coffee = new Coffee()
coffee.init()

泡一杯茶步骤:

  1. 把水煮沸
  2. 用沸水浸泡茶叶
  3. 把茶水倒进杯子
  4. 加柠檬
javascript
const Tea = function () {

}

Tea.prototype.boilWater = function () {
  console.log('把水煮沸')
}

Tea.prototype.steepTeaBag = function () {
  console.log('用沸水浸泡茶叶')
}

Tea.prototype.pourInCup = function () {
  console.log('把茶水倒进杯子')
}

Tea.prototype.addLemon = function () {
  console.log('加柠檬')
}

Tea.prototype.init = function () {
  this.boilWater()
  this.brewCoffeeGriends()
  this.pourInCup()
  this.addSugarAndMilk()
}

const tea = new Tea()
tea.init()

仔细观察这两个,发现冲咖啡和泡茶无非大同小异

  1. 都要煮水
  2. 只是原料不同,一个是咖啡一个是茶叶,我们可以把它们都抽象成“饮料”
  3. 都要倒到杯子
  4. 加的调料不一样,一个是糖和牛奶一个是柠檬,我们可以把它们都抽象成“调料”

因此我们可以创建一个抽象的父类来表示这泡一杯饮料的过程,无论是咖啡还是茶。

javascript
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();
};

则咖啡的表示方法:

javascript
Coffee.prototype = new Beverage()

Coffee.prototype.brew = function () {
  console.log('用沸水冲咖啡')
}

Coffee.prototype.pourInCup = function () {
  console.log('把咖啡倒进杯子')
}

Coffee.prototype.addCondiments = function () {
  console.log('加糖和牛奶')
}

const coffee = new Coffee()
coffee.init()

省略tea的实现,照葫芦画瓢即可。

而在这个例子中,Beverage.prototype.init 就是模板方法。因为该方法中封装了子类的算法框架,指导子类以何种顺序去执行哪些方法

在上面的例子中看似很完美,实际隐藏着危险。如果这个类是在你编写以及你记忆力强的情况下,你子类都重写了父类的方法。但如果这个类不是你写的,或者你忘记了重写某些方法呢?在 JavaScript 中依旧能正常运行,但它变为空,例如上面例子,泡茶的时候我忘记重写 brew 方法,变成煮完水,就有咖啡或者茶倒到杯子里了。

Java 这类静态语言中,会有编译器帮助检查类型,而 JavaScript 是门 “类型模糊” 的语言,在模拟继承的时候,没有编译器帮助我们任何形式的检查,没办法保证子类会重写父类中的 ”抽象方法“。

解决方法:我们可以在抽象方法内部抛出一个错误

javascript
Beverage.prototype.brew = function() {    
  throw new Error('子类必须重写brew方法')
}

构子方法

在模板方法模式,在父类中封装了子类的算法框架,在大多数情况下适合多数子类使用,但当遇到一个“特别”的子类呢?

比如:加调料这一步骤不是每人都需要的,可能有一些客人不喜欢加糖和牛奶。

这时候我们可以使用 构子方法(hook)来解决这个问题。

javascript
var 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()) { // 如果构子返回true,则需要调料
    this.addCondiments();
  }
}

const Coffee = function () {

}

Coffee.prototype = new Beverage()

Coffee.prototype.brew = function () {
  console.log('用沸水冲咖啡')
}

Coffee.prototype.pourInCup = function () {
  console.log('把咖啡倒进杯子')
}

Coffee.prototype.addCondiments = function () {
  console.log('加糖和牛奶')
}

Coffee.prototype.customerWantsCondiments = function () {
  return window.confirm('请问需要调料吗?')
}

const coffee = new Coffee()
coffee.init()

好莱坞原理

好莱坞也有很多找不到工作的新人演员,许多新人演员在好莱坞把简历递给演艺公司之后就只有回家等待电话。有时候该演员等得不耐烦了,给演艺公司打电话询问情况,演艺公司往往这样回答:“不要来找我,我会给你打电话。” 这样的规则称为好莱坞原理

模板方法模式 是 好莱坞原理 的一个典型使用场景。当使用模板方法模式编写一个程序时,就意味着子类放弃了对自己的控制权,而是改为父类通知子类,哪些方法应该在上面时候被调用。作为子类,只负责听过一些设计上的细节。即高层组件调用底层组件。

Released under the MIT License.