作者:葉@毛豆前端

一、定义

模板方法模式定义了一个算法的步骤,并允许子类别为一个或多个步骤提供其实践方式。让子类别在不改变算法架构的情况下,重新定义算法中的某些步骤

以上的定义可以知道模板方法模式由两部分组成

  1. 抽象的实现算法(抽象类)
  2. 子类的具体实现方法(实现类)

模板方式将是共性的部分放在父类中,不同的部分放在子类中依据不同的情况分别实现。这样的实现方式可以避免重复的行为在各个子类中冗余

二、例子

《Head First设计模式》中讲到coffee or tea这个例子是个经典的模板方法模式,作为一个资深吃货,由于总吃外卖,且在北方生活基本吃干饭,一个南方人,总是对于吃饭喝汤的执着甚深,也愿意自己动手煲汤,发现炖汤做菜也是些符合模板方法模式->_->!! 果然吃货的脑回路就是不一样,哈哈。接下来看下我煲玉米排骨汤和牛肉萝卜汤的的例子:

玉米排骨汤

通常炖玉米排骨汤的步骤是这样的:

  1. 用凉水把排骨焯一遍
  2. 捞出放炖盅,加上生姜、料酒、玉米
  3. 煮一小时
  4. 排骨玉米汤盛碗里

我们用代码大致模拟下这个过程:

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

至此,一份排骨玉米汤就完成啦!我们看下炖牛肉萝卜汤是怎么样的一个过程:

牛肉萝卜汤

  1. 用凉水把牛肉焯一遍
  2. 捞出放炖盅,加入葱姜蒜、桂皮、香叶、花椒、萝卜
  3. 煮四十分钟
  4. 牛肉萝卜汤盛碗里

我们仍然用代码大致表示下:

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

现在我们把两个的过程都用代码大致表示出来了,我们也能从中发现一些两者之间的相似点,我们做个比较与总结

比较与总结

我们看下这两者之前存在着那些不同的地方:

  1. 煮汤的主原料是不一样的,一个是排骨玉米,另一个牛肉萝卜,我们可以统称为这些为”食材”
  2. 煮的过程中添加的调味剂也不大不相同,牛肉萝卜放了好多的香料,我们也统一下,都称之为”辅料”
  3. 煮的过程中,排骨需要煮上一小时,牛肉只需要40分钟,但是动作都是”煮”

分离出了不同之处,那给这两个过程做一个统一:

  1. 原材料焯水
  2. 添加辅料
  3. 煮熟
  4. 盛碗里

发现了吧,是不是很符合我们说的模板方法模式的定义,把上面总结的统一过程作为一个实现做汤的”算法”,具体做什么汤,分别实现。现在我们就要运用模板方法来模式实现下前面两个做汤的过程,在此,你可以先忘记上面是如何实现的:

首先,我们先建立一个做汤的父类,实现这个算法:

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. 何为抽象类

抽象类往往用来表征对问题领域进行分析、设计中得出的抽象概念,是对一系列看上去不同,但是本质上相同的具体概念的抽象。抽象类是不完整的,它只能用作基类。在面向对象方法中,抽象类主要用来进行类型隐藏和充当全局变量的角色。

划重点:
  1. 对一系列看上去不同,但是本质上相同的具体概念的抽象
  2. 只能用作基类,主要用来进行类型隐藏和充当全局变量的角色

对于第一点很好理解吧,上面我们的例子也就说明了这一点,看上去两种做汤方式确实不一样,但是,归纳差异点之后,我们也能抽象出init这个模板方法,来实现本质相同。

第二点,在面向对象的语言中,抽象类是不能被实例化的,继承了某个抽象类,那么它的子类都将拥有跟抽象类一样的接口方法,是为他的子类定义公共接口。我们可以构造出一个固定的一组行为的抽象描述,但是这组行为却能够有任意个可能的具体实现方式,即它是在产生子类的同时给予子类一些特定的属性和方法。

2.javascript 模拟缺陷

实际上抽象类在JavaScript语言层面上并没有提供支持,前面我们使用的是原型继承的方式来模拟类继承,也并没有真正意义上的实现,因为在面向对象语言中,当子类继承了某个抽象类时,是必须重写父类的抽象方法,否则编译时不通过,javascript中没有这些检查,因此实现代码的时候需要人为干预,这样的做法是很不安全的,我们可以在父类的抽象方法中直接抛出一个异常:

MakeSoup.prototype.blanching = function() {
    console.error("子类必须重写blanching方法!");
}

类似的,每个抽象方法都手动抛出错误。

四、总结

模板方法模式是非常典型的用封装来提高系统拓展性的设计模式,设计了模板方法模式的代码中,子类拥有的属性和方法的执行顺序都是被确定的,在后来的拓展中,我们只要增加新的子类,就可以增加系统的新功能,而无需改动抽象类的代码。在javascript中,模拟模板方法模式固然有时候也不错,但是还有一个可能会是更好的选择,即高阶函数。


毛豆前端团队

never stop, always on the way!