JS开发用到继承的地方并不多,模板方法模式是基于继承的一种设计模式。不同的类型可能存在同样的执行步骤,那么就可以将各个步骤抽离出来,定义在一个类中,各个类型再去继承这个类。
模板方法模式由 2 个部分组成,抽象父类、具体的实现子类
。抽象父类包含了子类共有的方法,并且定义了init方法,封装了子类各个方法的执行顺序。具体的实现子类继承自父类,但可以选择重写父类方法,实现自己与其他子类不同的地方。
父类中,定义了子类完全相同的方法,子类之间不同的方法只实现为空函数,由子类自己重写。
// 咖啡与茶
// 定义 Coffee、Tea
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();
var 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.steepTeaBag();
this.pourInCup();
this.addLemon();
}
var tea = new Tea();
tea.init();
// 2.理清不同点,抽离共同点
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();
}
// 3.定义子类,继承父类,选择重写独属于自己的方法
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();
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();
抽象类和接口有 2 个作用,向上转型、为子类定义公共接口
。向上转型是为了解耦对象与类型,避免类型的强制检查,将不同类型的对象可以相互交换使用。在静态类型语言中,子类必须实现抽象父类的抽象方法,否则无法编译成功。
抽象类中有 2 种方法,抽象方法、具体方法
。比如,咖啡与茶中的brew就是具体方法,其他方法就是抽象方法。
JS 没有抽象类,也没有类型检查帮助我们检测子类是否重写了抽象方法。对于JS来说,有 2 种解决方案,鸭子类型模拟接口检查、在抽象方法中抛出异常
。
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');
}
使用场景有大有小,构建项目框架(HttpServlet
有 7 个生命周期方法,还有一个service方法 – 模板方法,规定了生命周期方法的执行顺序)、构建UI
组件(一般有 4 个步骤)。
钩子方法 hook
,可以改变模板方法中规定的算法执行顺序。在父类的最容易变化的地方放置钩子,暴露给子类,可由子类控制是否需要挂钩。
// 钩子方法
var Beverage = function() {}
Beverage.prototype.boilWater = function() {
console.log('把水煮沸');
}
Beverage.prototype.brew = function() {}
Beverage.prototype.pourInCup = function() {}
Beverage.prototype.addCondiments = function() {}
// hook 子类可以重写该方法,决定 “加糖和牛奶” 方法是否执行。
Beverage.prototype.customerWantsCondiments = function() {
return true;
}
Beverage.prototype.init = function() {
this.boilWater();
this.brew();
this.pourInCup();
if (this.customerWantsCondiments()) {
this.addCondiments();
}
}
var CoffeWithHook = function() {}
CoffeWithHook.prototype = new Beverage;
CoffeWithHook.prototype.brew = function() {
console.log('用沸水冲泡咖啡');
}
CoffeWithHook.prototype.pourInCup = function() {
console.log('将咖啡倒进杯子');
}
CoffeWithHook.prototype.addCondiments = function() {
console.log('加糖和牛奶');
}
CoffeWithHook.prototype.customerWantsCondiments = function() {
return window.confirm('请问需要调料吗');
}
var coffeeWithHook = new CoffeWithHook;
coffeeWithHook.init();
模板方法模式
体现了一个原则 – 好莱坞原则
。发布-订阅、回调
同样体现了这一原则。好莱坞原则里有 2 个角色,简单描述为上下级,下级由上级控制调用,下级只负责细节上的实现
。映射到模板方法模式
就是子类放弃调用的控制权,交由父类控制,只负责细节实现部分。
之前的例子都是在形式上模拟继承,在JS
中可以使用高阶函数
实现。
const Beverage = function(param) {
const boilWater = function() {
console.log('把水煮沸');
}
const brew = param.brew || function() {
console.log('必须传递 brew 方法');
}
const pourInCup = param.pourInCup || function() {
console.log('必须传递 pourInCup 方法');
}
const addCondiments = param.addCondiments || function() {
console.log('必须传递 addCondiments');
}
const F = function() {}
F.prototype.init = function() {
boilWater();
brew();
pourInCup();
addCondiments();
}
return F;
};
const Coffee = Beverage({
brew() {
console.log('用沸水冲泡咖啡');
},
pourInCup() {
console.log('将咖啡倒进杯子');
},
addCondiments() {
console.log('加糖和牛奶');
}
});
const coffee = new Coffee;
coffee.init();