JavaScript模板方法模式

JavaScript模板方法模式

  • 1 什么是模板方法模式
  • 2 Coffee or Tea
  • 3 钩子方法
  • 4 好莱坞原则

1 什么是模板方法模式

模板方法模式是一种只需使用继承就可以实现的模式。

模板方法模式由两部分结构组成,第一部分是抽象父类,第二部分是具体的实现子类。

通常在抽象父类中封装了子类的算法框架,包括实现一些公共方法以及封装子类中所有方法的执行顺序。子类通过继承这个抽象类,也继承了整个算法结构,并且可以选择重写父类的方法。

2 Coffee or Tea

接下来通过咖啡与茶这个经典的例子来说明模版方法模式。

首先,我们想泡一杯咖啡,泡咖啡的步骤通常如下:

  1. 把水煮沸
  2. 用沸水冲泡咖啡
  3. 将咖啡倒入杯子中
  4. 添加糖和牛奶

将上述步骤写成代码描述出来,具体如下:

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

接下来,我们泡一壶茶,泡茶的步骤和泡咖啡的步骤基本相同:

  1. 把水煮沸
  2. 用沸水浸泡茶叶
  3. 将茶水倒入杯子中
  4. 添加柠檬

同样用一段代码来实现泡茶的步骤:

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

经过对比泡咖啡和泡茶的过程,我们发现两者的步骤好像差不多,但是有如下不同点:

  • 原料不同。一个是咖啡,一个是茶,但我们可以把它们都抽象为“饮料”;
  • 泡的方式不同。咖啡是冲泡,而茶叶是浸泡,我们可以把它们都抽象为“泡”;
  • 加入的调料不同。一个是糖和牛奶,一个是柠檬,但我们可以把它们都抽象为“调料”;

经过抽象之后,不管是泡咖啡还是泡茶,我们都能整理为下面四步:

  1. 把水煮沸
  2. 用沸水冲泡饮料
  3. 将饮料倒入杯子中
  4. 添加调味料

根据上述共同点,我们可以创建一个抽象父类来表示泡一杯饮料的整个过程。不论是Coffee,还是Tea,都用Beverage来表示,代码如下:

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.steepTeaBag = function () {
  console.log("用沸水浸泡茶叶");
};
Tea.prototype.pourInCup = function () {
  console.log("把茶水倒进杯子");
};
Tea.prototype.addCondiments = function () {
  console.log("加柠檬");
};

var tea = new Tea();
tea.init();

在上面的例子中,Beverage.prototype.init就是模板方法,因为该方法中封装了子类的算法框架,它作为一个算法的模板,指导子类以何种顺序去执行哪些方法。

3 钩子方法

通过模板方法模式,我们在父类中封装了子类的算法框架。这些算法框架在正常状态下是适用于大多数子类的,但如果有一些特别“个性”的子类呢?比如有一些客人喝咖啡是不加调料(糖和牛奶)的。

既然Beverage作为父类,已经规定好了冲泡饮料的4个步骤,那么有什么办法可以让子类不受这个约束呢?

钩子方法可以用来解决这个问题,放置钩子是隔离变化的一种常见手段。我们在父类中容易变化的地方放置钩子,钩子可以有一个默认的实现,究竟要不要“挂钩”,这由子类自行决定。钩子方法的返回结果决定了模板方法后面部分的执行步骤,也就是程序接下来的走向,这样一来,程序就拥有了变化的可能。

接下来将挂钩customerWantsCondiments放入Beverage类,看看我们如何得到一杯不需要糖和牛奶的咖啡,代码如下:

var Beverage = function () {};
Beverage.prototype.boilWater = function () {
  console.log("把水煮沸");
};
Beverage.prototype.brew = function () {}; // 冲泡动作
Beverage.prototype.pourInCup = function () {}; // 倒入杯子
Beverage.prototype.addCondiments = function () {}; // 添加调料
Beverage.prototype.customerWantsCondiments = function () {
  return true; // 默认需要调料
};
Beverage.prototype.init = function () {
  this.boilWater();
  this.brew();
  this.pourInCup();
  if (this.customerWantsCondiments()) { // 如果返回true,表示需要调料
    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("加糖和牛奶");
};
Coffee.prototype.customerWantsCondiments = function () {
  return window.confirm("请问需要调料吗?");
};
var Coffee = new Coffee();
Coffee.init();

4 好莱坞原则

JS好莱坞原则的基本思想是,高层组件通过调用底层组件暴露的接口来控制底层组件的行为,而底层组件不应该直接依赖或调用高层组件的实现。

这个原则的名字来源于好莱坞电影制片厂,因为在好莱坞电影中,明星演员通常是高层组件,而服装、化妆等其他组件则是底层组件。这些底层组件在电影制作过程中由高层组件来控制和协调。

在这一原则的指导下,我们允许底层组件将自己挂钩到高层组件中,而高层组件会决定什么时候、以何种方式去使用这些底层组件。

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

在好莱坞原则的指导之下,下面这段代码可以达到和继承一样的效果。

var Beverage = function (param) {
  var boilWater = function () {
    console.log("把水煮沸");
  };
  var brew = param.brew; // 冲泡动作
  var pourInCup = param.pourInCup; // 倒入杯子
  var addCondiments = param.addCondiments; // 添加调料
  var F = function () {};
  F.prototype.init = function () {
    boilWater();
    brew();
    pourInCup();
    addCondiments();
  };
  return F;
};

var Coffee = Beverage({
  brew: function () {
    console.log("用沸水冲泡咖啡");
  },
  pourInCup: function () {
    console.log("把咖啡倒进杯子");
  },
  addCondiments: function () {
    console.log("加糖和牛奶");
  },
});

var Tea = Beverage({
  brew: function () {
    console.log("用沸水浸泡茶叶");
  },
  pourInCup: function () {
    console.log("把茶倒进杯子");
  },
  addCondiments: function () {
    console.log("加柠檬");
  },
});

var coffee = new Coffee();
coffee.init();
var tea = new Tea();
tea.init();

在这段代码中,我们把brewpourInCupaddCondiments这些方法依次传入Beverage函数,Beverage函数被调用之后返回构造器FF类中包含了“模板方法”F.prototype.init。跟继承得到的效果一样,该“模板方法”里依然封装了饮料子类的算法框架。

你可能感兴趣的:(JavaScript,javascript,模板方法模式,开发语言)