作者:葉@毛豆前端
一、定义
模板方法模式定义了一个算法的步骤,并允许子类别为一个或多个步骤提供其实践方式。让子类别在不改变算法架构的情况下,重新定义算法中的某些步骤
以上的定义可以知道模板方法模式由两部分组成
- 抽象的实现算法(抽象类)
- 子类的具体实现方法(实现类)
模板方式将是共性的部分放在父类中,不同的部分放在子类中依据不同的情况分别实现。这样的实现方式可以避免重复的行为在各个子类中冗余
二、例子
《Head First设计模式》中讲到coffee or tea这个例子是个经典的模板方法模式,作为一个资深吃货,由于总吃外卖,且在北方生活基本吃干饭,一个南方人,总是对于吃饭喝汤的执着甚深,也愿意自己动手煲汤,发现炖汤做菜也是些符合模板方法模式->_->!! 果然吃货的脑回路就是不一样,哈哈。接下来看下我煲玉米排骨汤和牛肉萝卜汤的的例子:
玉米排骨汤
通常炖玉米排骨汤的步骤是这样的:
- 用凉水把排骨焯一遍
- 捞出放炖盅,加上生姜、料酒、玉米
- 煮一小时
- 排骨玉米汤盛碗里
我们用代码大致模拟下这个过程:
let Yumipaigu = function(){}
Yumipaigu.prototype.chaopaigu = function() {
console.log('排骨焯水');
}
Yumipaigu.prototype.addYumi = function() {
console.log('加生姜、料酒、玉米');
}
Yumipaigu.prototype.boil = function(){
console.log('煮一小时');
}
Yumipaigu.prototype.pourInBowl = function() {
console.log('排骨玉米汤盛碗里');
}
Yumipaigu.prototype.init = function() {
this.chaopaigu();
this.addYumi();
this.boil();
this.pourInBowl();
}
let yumipaigu = new Yumipaigu();
yumipaigu.init();
至此,一份排骨玉米汤就完成啦!我们看下炖牛肉萝卜汤是怎么样的一个过程:
牛肉萝卜汤
- 用凉水把牛肉焯一遍
- 捞出放炖盅,加入葱姜蒜、桂皮、香叶、花椒、萝卜
- 煮四十分钟
- 牛肉萝卜汤盛碗里
我们仍然用代码大致表示下:
let Niurouluobo = function(){}
Niurouluobo.prototype.chaoniurou = function() {
console.log('牛肉焯水');
}
Niurouluobo.prototype.addNiurou = function() {
console.log('加入葱姜蒜、桂皮、香叶、花椒、萝卜');
}
Niurouluobo.prototype.boil = function(){
console.log('煮四十分钟');
}
Niurouluobo.prototype.pourInBowl = function() {
console.log('牛肉萝卜汤盛碗里');
}
Niurouluobo.prototype.init = function() {
this.chaoniurou();
this.addNiurou();
this.boil();
this.pourInBowl();
}
let niurouluobo = new Niurouluobo();
niurouluobo.init();
现在我们把两个的过程都用代码大致表示出来了,我们也能从中发现一些两者之间的相似点,我们做个比较与总结
比较与总结
我们看下这两者之前存在着那些不同的地方:
- 煮汤的主原料是不一样的,一个是排骨玉米,另一个牛肉萝卜,我们可以统称为这些为”食材”
- 煮的过程中添加的调味剂也不大不相同,牛肉萝卜放了好多的香料,我们也统一下,都称之为”辅料”
- 煮的过程中,排骨需要煮上一小时,牛肉只需要40分钟,但是动作都是”煮”
分离出了不同之处,那给这两个过程做一个统一:
- 原材料焯水
- 添加辅料
- 煮熟
- 盛碗里
发现了吧,是不是很符合我们说的模板方法模式的定义,把上面总结的统一过程作为一个实现做汤的”算法”,具体做什么汤,分别实现。现在我们就要运用模板方法来模式实现下前面两个做汤的过程,在此,你可以先忘记上面是如何实现的:
首先,我们先建立一个做汤的父类,实现这个算法:
let MakeSoup = function() {}
MakeSoup.prototype.blanching = function() {} // 空方法,由子类重写
MakeSoup.prototype.addExcipients = function() {} // 空方法,由子类重写
MakeSoup.prototype.cooked = function() {} // 空方法,由子类重写
MakeSoup.prototype. intoBowl = function() {} // 空方法,由子类重写
MakeSoup.prototype.init = function () {
this.blanching()
this.addExcipients()
this.cooked()
this.intoBowl()
}
现在这个MakeSoup类就算是实现了,但是只有这个类并不能做出什么具体的汤,因为说了,这个父类只是提供了一个抽象的算法步骤,并没有真正的意义,因此我们还要根据具体的内容实现我们所需要的内容
接下来分别实现下排骨玉米汤和牛肉萝卜这两个类:这两个类需要先继承MakeSoup,然后按照里面的步骤一步一步的重写实现
排骨玉米汤类
let Paiguyumi = new function()
Paiguyumi.prototype = new MakeSoup()
Paiguyumi.prototype.blanching = function () {
console.info("焯排骨")
}
Paiguyumi.prototype.addExcipients = function () {
console.info("添加生姜、料酒、玉米")
}
Paiguyumi.prototype.cooked = function () {
console.info("煮一小时")
}
Paiguyumi.prototype.intoBowl = function () {
console.info("排骨玉米盛碗里")
}
let paiguyumi = new Paiguyumi()
paiguyumi.init()
牛肉萝卜
let Niurouluobo = new function()
Niurouluobo.prototype = new MakeSoup()
Niurouluobo.prototype.blanching = function () {
console.info("焯牛肉")
}
Niurouluobo.prototype.addExcipients = function () {
console.info("加入葱姜蒜、桂皮、香叶、花椒、萝卜")
}
Niurouluobo.prototype.cooked = function () {
console.info("煮40分钟")
}
Niurouluobo.prototype.intoBowl = function () {
console.info("牛肉萝卜盛碗里")
}
let niurouluobo = new Niurouluobo()
niurouluobo.init()
目前我们的两个子类都模拟完了,当调用子类的init方法时,因为子类对象和构造器原型prototype上都没有对应的init方法,请求会顺着原型链找到父类的原型上对应的init方法。前面也说过了,模板方法,是封装了子类的实现算法,然后给子类的实现提供指引,告诉子类以什么样的顺序正确实执行哪些方法。在此例子中,MakeSoup.prototype.init自然便是我们的模板方法。
三、关于模板方法模式的一些解释说明
抽象类
1. 何为抽象类
抽象类往往用来表征对问题领域进行分析、设计中得出的抽象概念,是对一系列看上去不同,但是本质上相同的具体概念的抽象。抽象类是不完整的,它只能用作基类。在面向对象方法中,抽象类主要用来进行类型隐藏和充当全局变量的角色。
划重点:
- 对一系列看上去不同,但是本质上相同的具体概念的抽象
- 只能用作基类,主要用来进行类型隐藏和充当全局变量的角色
对于第一点很好理解吧,上面我们的例子也就说明了这一点,看上去两种做汤方式确实不一样,但是,归纳差异点之后,我们也能抽象出init这个模板方法,来实现本质相同。
第二点,在面向对象的语言中,抽象类是不能被实例化的,继承了某个抽象类,那么它的子类都将拥有跟抽象类一样的接口方法,是为他的子类定义公共接口。我们可以构造出一个固定的一组行为的抽象描述,但是这组行为却能够有任意个可能的具体实现方式,即它是在产生子类的同时给予子类一些特定的属性和方法。
2.javascript 模拟缺陷
实际上抽象类在JavaScript语言层面上并没有提供支持,前面我们使用的是原型继承的方式来模拟类继承,也并没有真正意义上的实现,因为在面向对象语言中,当子类继承了某个抽象类时,是必须重写父类的抽象方法,否则编译时不通过,javascript中没有这些检查,因此实现代码的时候需要人为干预,这样的做法是很不安全的,我们可以在父类的抽象方法中直接抛出一个异常:
MakeSoup.prototype.blanching = function() {
console.error("子类必须重写blanching方法!");
}
类似的,每个抽象方法都手动抛出错误。
四、总结
模板方法模式是非常典型的用封装来提高系统拓展性的设计模式,设计了模板方法模式的代码中,子类拥有的属性和方法的执行顺序都是被确定的,在后来的拓展中,我们只要增加新的子类,就可以增加系统的新功能,而无需改动抽象类的代码。在javascript中,模拟模板方法模式固然有时候也不错,但是还有一个可能会是更好的选择,即高阶函数。