模板方法模式
模板方法主要由两部分构成, 第一部分是抽象父类,第二部分是具体实现的子类,通常我们在抽象父类中封装子类的算法框架,子类通过继承这个抽象类,也继承了整个算法结构
要讲模板方法模式 首先来看一个著名的例子 Coffee or Tea,这个例子的原型来自于《Head First设计模式》
泡一杯咖啡的步骤
- 把水煮沸
- 用沸水冲泡咖啡
- 把咖啡倒进杯子
- 加糖和牛奶
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();
泡一壶茶的步骤
- 把水煮沸
- 用沸水浸泡茶叶
- 把茶叶倒进杯子
- 加柠檬
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();
对比这两件事情,我们发现他们的步骤是一样的。所以我们可以将这个步骤的算法封装到抽象父类中,然后在子类中来做当前步骤的具体实现
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();
};
创建Coffee子类
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();
这段代码做了个什么事情呢, Coffee继承了Beverage ,并且在自己的原型链上复写了Beverage.init()执行的方法,也就是泡一杯咖啡每一个步骤执行的具体方法。这样在执行初始化的时候各个步骤操作逻辑实现是由子类实现的,而逻辑的控制是由父类来控制的
我们用相同的方式创建tea子类
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();
我们讨论的是模板方法模式,那么上述代码那一块才能谓之曰模板方法呢? 答案是Beverage.prototype.init,这个方法中封装了子类算法框架,作为一个算法模板被子类调用
抽象类
模板方法模式是一种严重依赖于抽象类的设计模式,在Java中,类分为两种,一种是抽象类,一种是具体类。具体类可以被实例化,抽象类不能被实例化。在前面的例子中,比如茶,你可以说给我来个新的茶,是指的一类具体的事物,但是你不能说给我来个新的饮料,因为茶和咖啡都是饮料,所以抽象类不能被实例化。由于抽象类不能被实例化,如果有人编写了一个抽象类,那么这个抽象类一定是用来被某些 具体类继承的
在静态类型语言中, 编译器对类型的检查总是一个绕不过的话题与困扰。虽然类型检查可以提高程序的安全性,但繁 琐而严格的类型检查也时常会让程序员觉得麻烦。把对象的真正类型隐藏在抽象类或者接口之 后,这些对象才可以被互相替换使用。这可以让我们的 Java 程序尽量遵守依赖倒置原则。除了用于向上转型,抽象类也可以表示一种契约。继承了这个抽象类的所有子类都将拥有跟 抽象类一致的接口方法,抽象类的主要作用就是为它的子类定义这些公共接口。如果我们在子类 中删掉了这些方法中的某一个,那么将不能通过编译器的检查,这在某些场景下是非常有用的。
JavaScript 没有抽象类的缺点和解决方案
当我们在 JavaScript 中使用原型继承来模拟传统的类式继承时,并没有编译器帮 助我们进行任何形式的检查,我们也没有办法保证子类会重写父类中的“抽象方法”。
如果我们的 Coffee 类或者 Tea 类忘记实现这 4个方法中的一个呢?拿 brew 方法举例,如果 我们忘记编写 Coffee.prototype.brew 方法,那么当请求 coffee 对象的 brew 时,请求会顺着原型 链找到 Beverage“父类”对应的 Beverage.prototype.brew 方法,而 Beverage.prototype.brew 方法 到目前为止是一个空方法,这显然是不能符合我们需要的
在 Java 中编译器会保证子类会重写父类中的抽象方法,但在 JavaScript 中却没有进行这些检 查工作。我们在编写代码的时候得不到任何形式的警告,完全寄托于程序员的记忆力和自觉性是 很危险的,特别是当我们使用模板方法模式这种完全依赖继承而实现的设计模式时
提供两种变通的解决方案
- 第 1 种方案是用鸭子类型来模拟接口检查,以便确保子类中确实重写了父类的方法。但模拟接口检查会带来不必要的复杂性,而且要求程序员主动进行这些接口检查,这就要求
我们在业务代码中添加一些跟业务逻辑无关的代码。 - 第 2 种方案是让 Beverage.prototype.brew 等方法直接抛出一个异常,如果因为粗心忘记编写 Coffee.prototype.brew 方法,那么至少我们会在程序运行时得到一个错误.
Beverage.prototype.brew = function(){
throw new Error( '子类必须重写 brew 方法' );
};
Beverage.prototype.pourInCup = function(){
throw new Error( '子类必须重写 pourInCup 方法' );
};
Beverage.prototype.addCondiments = function(){
throw new Error( '子类必须重写 addCondiments 方法');
}
第 2 种解决方案的优点是实现简单,付出的额外代价很少;缺点是我们得到错误信息的时间点太靠后。
本文部分摘录自 曾探《javascript 设计模式与开发实践》这本书写得相当出彩 强烈推荐
我的博客 https://yangfan0095.github.io