模板方法模式
介绍
- 只需要使用继承就可以实现
- 子类实现中的相同部分被上移到父类中,将不同的部分留待子类实现,体现泛化的思想
构成
- 抽象父类
- 具体的实现子类
通常在抽象父类中封装子类的算法框架,包括实现一些公共方法以及封装子类中所有方法的执行顺序。子类通过继承这个抽象类,也就继承了整个算法结构,并且可以选择重写父类的方法。
示例
Coffee or Tea
☕️泡一杯拿铁
泡拿铁的步骤如下
- 把水煮沸
- 用热水冲泡咖啡
- 把咖啡倒进杯子
- 加牛奶
class Coffee {
boilWater() {
console.log('把水煮沸')
}
brewCoffee() {
console.log('用热水冲泡咖啡')
}
pourInCup() {
console.log('把咖啡倒进杯子')
}
addMilk() {
console.log('加牛奶🥛')
}
init() {
this.boilWater()
this.brewCoffee()
this.pourInCup()
this.addMilk()
}
}
const coffee = new Coffee()
coffee.init()
🍵泡一壶茶
泡壶茶的步骤如下:
- 把水煮沸
- 用沸水浸泡茶叶
- 把茶水倒进杯子
- 加柠檬
class Tea {
boilWater() {
console.log('把水煮沸')
}
brewTea() {
console.log('用热水浸泡茶叶')
}
pourInCup() {
console.log('把茶水倒进杯子')
}
addLemon() {
console.log('加柠檬🍋')
}
init() {
this.boilWater()
this.brewTea()
this.pourInCup()
this.addLemon()
}
}
const tea = new Tea()
tea.init()
分离共同点
我们发现拿铁和茶的冲泡过程好像差不多嘛,不同点只有 3 个:
- 原料不同,咖啡和茶,可以抽象为“饮料”
- 泡的方式不同,咖啡是冲泡,茶叶是浸泡。都可以抽象为“泡”
- 加入的调理不同,咖啡是牛奶,茶叶是柠檬,都可以抽象为“调料”
这样抽象完成后就得到了下面的步骤👇🏻
- 把水煮沸
- 用热水泡饮料
- 把饮料倒进杯子
- 加调料
让我们忘记刚才创建的 Coffee 类和 Tea 类,使用抽象类 Beverage
class Beverage {
boilWater() {
console.log('把水煮沸')
}
// 空方法,由子类重写
brew() {}
// 空方法, 由子类重写
pourInCup() {}
// 空方法, 由子类重写
addCondiments() {}
init() {
this.boilWater()
this.brew()
this.pourInCup()
this.addCondiments()
}
}
创建 Coffee 子类和 Tea 子类
class Coffee extends Beverage {
constructor() {
super()
}
brew() {
console.log('用热水冲泡咖啡')
}
pourInCup() {
console.log('把咖啡倒进杯子')
}
addCondiments() {
console.log('加牛奶🥛')
}
}
class Tea extends Beverage {
constructor() {
super()
}
brew() {
console.log('用热水浸泡茶叶')
}
pourInCup() {
console.log('把茶水倒进杯子')
}
addCondiments() {
console.log('加柠檬🍋')
}
}
const coffee = new Coffee()
coffee.init()
const tea = new Tea()
tea.init()
上面的 init
方法就是 模板方法
, 因为该方法中封装了子类的框架算法,它作为一个算法的模板,知道子类以何种顺序去执行某些方法。
钩子方法
上面咖啡和茶的例子中,我们通过模板方法模式在父类中封装了子类地方算法框架。但是如果有一些特殊情况,比如有的子类不需要加调料,olu 喝咖啡就很少加奶,喝茶也不会加柠檬。这时 Beverage
父类规定好的 4 个冲泡饮料步骤就有冲突了,我们的子类需要摆脱父类的约束。
钩子方法(hook
可以解决这个问题,是否需要“挂钩”来放置钩子可以由子类自行决定。钩子方法的返回结果决定了模板方法后面的执行。放置钩子是隔离变化的常见手段。
在这个例子中我们把挂钩的名字定位 customerWantsCondiments, 并放入 Beverage 类,得到一杯不需要牛奶的美式咖啡☕️
class Beverage {
boilWater() {
console.log('把水煮沸')
}
// 空方法,由子类重写
brew() {
throw new Error(`子类必须重写 brew 方法`)
}
// 空方法, 由子类重写
pourInCup() {
throw new Error(`子类必须重写 pourInCup 方法`)
}
// 空方法, 由子类重写
addCondiments() {
throw new Error(`子类必须重写 addCondiments 方法`)
}
customerWantsCondiments() {
return true // 默认需要调料
}
init() {
this.boilWater()
this.brew()
this.pourInCup()
if (this.customerWantsCondiments()) {
this.addCondiments()
}
}
}
class CoffeeWithHook extends Beverage {
constructor() {
super()
}
brew() {
console.log('用热水冲泡咖啡')
}
pourInCup() {
console.log('把咖啡倒进杯子')
}
addCondiments() {
console.log('加牛奶🥛')
}
customerWantsCondiments() {
return window.confirm('需要加奶吗')
}
}
const coffeeWithHook = new CoffeeWithHook()
coffeeWithHook.init()
JavaScript 中继承不是必须的
class Beverage {
constructor(param) {
this.param = param || {}
}
boilWater() {
console.log('把水煮沸')
}
brew() {
if (this.param.brew) {
return this.param.brew()
}
throw new Error('子类必须传递 brew 方法')
}
pourInCup() {
if (this.param.pourInCup) {
return this.param.pourInCup()
}
throw new Error('子类必须传递 pourInCup 方法')
}
addCondiments() {
if (this.param.addCondiments) {
return this.param.addCondiments()
}
throw new Error('子类必须传递 addCondiments 方法')
}
customerWantsCondiments() {
if (this.param.customerWantsCondiments) {
return this.param.customerWantsCondiments()
}
return true
}
init () {
this.boilWater()
this.brew()
this.pourInCup()
if (this.customerWantsCondiments()) {
this.addCondiments()
}
}
}
const coffeeWithHook = new Beverage({
brew: () => {
console.log('用热水冲泡咖啡')
},
pourInCup: () => {
console.log('把咖啡倒进杯子')
},
addCondiments: () => {
console.log('加牛奶🥛')
},
customerWantsCondiments: () => {
return window.confirm('需要加奶吗')
}
})
coffeeWithHook.init()
const teaWithHook = new Beverage({
brew: () => {
console.log('用热水浸泡茶叶')
},
pourInCup: () => {
console.log('把茶水倒进杯子')
},
addCondiments: () => {
console.log('加柠檬🍋')
},
customerWantsCondiments: () => {
return window.confirm('需要加柠檬吗')
}
})
teaWithHook.init()