编写可维护性的JavaScript 之设计模式 (二)

模板方法模式

模板方法主要由两部分构成, 第一部分是抽象父类,第二部分是具体实现的子类,通常我们在抽象父类中封装子类的算法框架,子类通过继承这个抽象类,也继承了整个算法结构
要讲模板方法模式 首先来看一个著名的例子 Coffee or Tea,这个例子的原型来自于《Head First设计模式》
泡一杯咖啡的步骤

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

对比这两件事情,我们发现他们的步骤是一样的。所以我们可以将这个步骤的算法封装到抽象父类中,然后在子类中来做当前步骤的具体实现

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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,456评论 5 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,370评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,337评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,583评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,596评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,572评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,936评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,595评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,850评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,601评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,685评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,371评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,951评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,934评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,167评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,636评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,411评论 2 342

推荐阅读更多精彩内容

  • 单例模式 适用场景:可能会在场景中使用到对象,但只有一个实例,加载时并不主动创建,需要时才创建 最常见的单例模式,...
    Obeing阅读 2,053评论 1 10
  • 工厂模式类似于现实生活中的工厂可以产生大量相似的商品,去做同样的事情,实现同样的效果;这时候需要使用工厂模式。简单...
    舟渔行舟阅读 7,716评论 2 17
  • 在 JavaScript 开发中用到继承的场景其实并不是很多,很多时候我们都喜欢用 mix-in 的方式给对象扩展...
    风铭阅读 377评论 0 0
  • 设计模式基本原则 开放-封闭原则(OCP),是说软件实体(类、模块、函数等等)应该可以拓展,但是不可修改。开-闭原...
    西山薄凉阅读 3,748评论 3 13
  • 面向对象编程 1.创建,使用函数 var CheckObject = {checkName : function(...
    依米花1993阅读 383评论 0 0