设计模式——模板方法模式
模板方法模式是一种基于继承的设计模式。事件万物总有相似之处,提取出相似点作为父类,提取出不同点作为子类来继承父类,就得到了有着相同特点的不同对象。然后将方法骨架延迟到子类中,这个方法就是针对这一类相同特点对象的模板方法。这种设计模式很好的体现了泛化的思想。
模板方法实例
以一个经典的冲泡饮料的例子来分析,我们都知道冲泡的步骤是可以提取出相同部分的,但是根据饮料的不同还要分离出不同的部分。我们先实现相同部分,也就是父类的定义。
let Beverage = function() {
};
Beverage.prototype.boilWater = function() {
console.log('把水煮沸');
}
Beverage.prototype.brew = function() {
console.log('冲泡');
}
Beverage.prototype.pourInCup = function() {
console.log('把饮料倒进杯子');
}
Beverage.prototype.addCondiments = function() {
console.log('加调料');
}
Beverage.prototype.template = function() {
this.boilWater();
this.brew();
this.pourInCup();
this.addCondiments();
}
这样我们就定义了一个抽象父类。当然仅有一个抽象类是无法创造出具体对象的,所以具体对象的不同之处我们通过继承的子类来定义。这里就创造咖啡这种具体子类,实际上当然可以有无数子类实现。
let Coffee = function() {
};
Coffee.prototype = new Beverage();
// 重写子类方法
Coffee.prototype.brew = function() {
console.log('沸水冲泡咖啡');
};
Coffee.prototype.pourInCup = function() {
console.log('把咖啡倒进杯子');
};
Coffee.prototype.addCondiments = function() {
console.log('加糖和牛奶');
}
至此就完成了一个继承子类,当调用继承到的模板方法时,将会结合继承方法和重写方法来实现具体的模板算法。
改进方案
现在我们虽然实现了一个模板方法模式,但还是存在一些缺点。下面来一一改进。
检查重写
可以看到实现具体子类时,都需要重写一些父类的方法,但在JS中并没有编译器来帮助我们进行类型检查,我们没办法保证子类重写了父类方法。下面就两种解决方案。
1.定义父类方法时模拟接口检查,创建子类时用来确保子类确实重写了方法。但这就要求了我们会在业务代码中增加一些和业务无关的检查代码。
2.定义父类方法时直接抛出异常。如果未进行重写就会直接抛出错误。这种方法虽然实现简单,也比较干净,但缺点在于必须等到程序运行时才能得到错误。
钩子方法
通过模板方法我们封装了子类的算法框架,但有些子类可能并不需要完整的算法,而只需要其中的某些步骤。比如有些咖啡就不加调料。通过钩子方法,我们就可以决定模板方法中算法的执行步骤。对上例的改进如下:
Beverage.prototype.customerWantsCondiments = function() {
return true; // 钩子方法默认需要调料
};
Beverage.prototype.init = function() {
this.boilWater();
this.brew();
this.pourInCup();
if (this.customerWantsCondiments()) {
this.addCondiments(); // 挂载到模板方法上
}
}
Coffee.prototype.customerWantsCondiments = function() {
return window.confirm('需要调料吗?') // 重写钩子方法根据返回值决定挂载结果
}
基于高阶函数的模板方法模式
由于在JS中函数为一级公民,所以完全可以通过闭包来定义一个带有默认父类方法的子类函数来模拟继承。将具体子类方法传入即可。
let Beverage = function(param) {
let boilWater = function() {
console.log('把水煮沸');
}
let {brew = function() {
throw new Error('必须传递brew方法');
}, pourInCup = function() {
throw new Error('必须传递pourInCup方法');
}, addCondiments = function() {
throw Error('必须传递addCondiments方法');
}} = param;
let F =function() {};
F.prototype.init = function() {
boilWater();
brew();
pourInCup();
addCondiments();
};
return F;
}
和继承实现的效果一样,模板方法依然封装了子类的算法框架。
小结
下列情景是适用模板方法模式需要注意的:
- 一次性实现一个算法不变的部分,然后将可变的算法部分留给子类实现来结合成一套完整算法。
- 当对象类型繁多时,可以考虑将公共部分提取到父类避免代码重用。
- 在JS中很多时候并不需要按照类的继承来套用模板方法模式,有时候高阶函数是更便利的选择。
-- EOF --