TIP

模板方式模式由两部分结构组成,第一部分是抽象父类,第二部分是具体的实现子类。通常在抽象父类中封装了子类的算法框架,包括实现一些公共方法以及封装子类中所有方法的执行顺序。子类通过继承这个抽象类,也继承了整个算法结构,并且可以选择重写父类的方法。

# 案例:咖啡与茶

泡一杯咖啡的步骤通常如下: (1) 把水煮沸 (2) 用沸水冲泡咖啡 (3) 把咖啡倒进杯子 (4) 加糖和牛奶

泡一杯茶的步骤相差不大: (1) 把水煮沸 (2) 用沸水浸泡茶叶 (3) 把茶水倒进杯子 (4) 加柠檬

观察泡咖啡和泡茶的步骤,可以找到他们的相同点和不同点。经过抽象之后,不管泡咖啡还是泡茶,都可以整理为以下四步: (1) 把水煮沸 (2) 用沸水冲泡饮料 (3) 把饮料倒进杯子 (4) 加调料

//抽象饮料类
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();
}

//咖啡类
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();

# 模版方法模式的使用场景

模板方法模式通常被架构师用于搭建项目的框架,架构师定好了框架的骨架,程序员继承框架的结构之后,负责往里面填空。

# 钩子方法

上年的饮料抽象类已经规定好了冲泡饮料的4个步骤,但是有的人可能不喜欢加调料(第4步),如果想让子类不受这个约束,我们可以实用化钩子方法(hook)来解决这个问题。
放置钩子是隔离变化的一种常见手段。在父类中容易变化的地方放置钩子,钩子可以有一个默认的实现,究竟要不要“挂钩”,这由子类自行决定。钩子方法的返回结果决定了模板方法后面部分的执行步骤,也就是程序接下来的走向,这样一来,程序就拥有了变化的可能。

//抽象饮料类
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方法");
}
      
Beverage.prototype.customerWantsCondiments = function () {
  return true;
}

Beverage.prototype.init = function () {
  this.boilWater();
  this.brew();
  this.pourInCup();
  if (this.customerWantsCondiments()) {
    this.addCondiments();
  }
}

//咖啡类
var Coffee = function () {};

Coffee.prototype = new Beverage();
        
Coffee.prototype.brew = function () {
  console.log("用水冲咖啡")
}
        
Coffee.prototype.pourInCup = function () {
  console.log("把咖啡倒进杯子里")
}

Coffee.prototype.customerWantsCondiments = function(){
  return window.confirm("是否加糖");
}

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

# 好莱坞原则

TIP

别调用我们,我们会调用你

模板方式模式是好莱坞原则的一个典型使用场景,它与好莱坞原来的联系非常明显,当我们用模板方法模式编写一个程序时,就意味着子类放弃了对自己的控制权,而是改为父类通知子类,哪些方法应该在什么时候调用。作为子类,只负责提供一些设计上的细节。

# 好莱坞原则的其他应用

  • 发布-订阅模式
  • 回调函数

# 真的需要“继承”吗

模板方法模式是基于继承的设计模式,但JavaScript语言实际上没有提供真正的类式继承,继承是通过对象与对象之间的委托来实现的。
下面是非继承实现的模板方法模式

//抽象饮料类
var Beverage = function (param) {
  var boilWater = function () {
      console.log("把水烧开");
  }
  var brew = param.brew || function () {
      throw new Error("子类必须重写父类的brew方法");
  }
  var pourInCup = param.pourInCup || function () {
      throw new Error("子类必须重写父类的pourInCup方法");
  }
  var addCondiments = param.addCondiments || function () {
      throw new Error("子类必须重写父类的addCondiments方法");
  }
  var customerWantsCondiments = param.customerWantsCondiments || function () {
      return true;
  }
  var F = function () {};
  F.prototype.init = function () {
      boilWater();
      brew();
      pourInCup();
      if (customerWantsCondiments()) {
          addCondiments();
      }
  }
  return F;
};

//咖啡类
var Coffee = Beverage({
  brew() {
      console.log("用水冲咖啡")
  },
  pourInCup() {
      console.log("把咖啡倒进杯子里");
  },
  addCondiments() {
      console.log("加糖");
  },
  customerWantsCondiments() {
      return window.confirm("是否加糖");
  }
})

var coffee = new Coffee();

coffee.init();

//茶类
var Tea = Beverage({
  brew() {
      console.log("用水冲茶叶")
  },
  pourInCup() {
      console.log("把茶倒进杯子里");
  },
  addCondiments() {
      console.log("加柠檬");
  },
  customerWantsCondiments() {
      return window.confirm("是否加柠檬");
  }
})

var tea = new Tea();

tea.init();

# 小结

模板方法模式是一种典型的通过封装变化来提高系统扩展性的设计模式。
在JavaScript中,我们可以使用高阶函数很方便的实现模板方法模式。