第二部分: 14种策略模式(下)

7.组合模式

首先,我们来回顾一下第6节中,我们讲的宏命令,我们很容易发现,宏命令包含了一系列的子命令,它们组成了一个树形结构,其中macroCommand被称为组合对象,closeDoorCommand,openPcCommand,openQQCommand被称为叶对象。在macroCommand的execute方法里面,并不执行真正的操作,而是遍历它所包含的叶对象,把真正的execute请求委托给这些叶对象.

组合模式的用途
  • 组合模式将对象组合成树形结构,以表示部分-整体的层次结构。
  • 通过对象的多态性,使得用户对单个对象和组合对象的使用具有一致性。
组合模式中请求在树种传递的过程

在组合模式中,请求在树中的传递过程总是遵循一种逻辑。
以宏命令为例,请求从树的顶端对象往下传递。如果子节点是叶对象,叶对象自身会处理这个请求。如果子节点还是组合对象,请求就是继续往下传递,直到树的尽头。对于客户而言,只需要关心树最顶层的组合对象。

透明性带来的安全问题

组合模式的透明性,使得发起请求的客户不用顾忌树中组合对象和叶对象的区别,但它们本质上还是有区别的:组合对象可以拥有子节点,叶对象下面没有子节点。所以就有可能存在一些误操作,解决办法是在叶对象中也添加同样的方法,并抛出异常来提醒客户。
比如我们之前的closeDoorCommand方法,可以更改为:

  var closeDoorCommand = {
    execute: function(){
      console.log('关门');
    },
    add: function(){
      throw new Error( '叶对象不能添加子节点' );
    }
  }
组合模式的例子--扫描文件夹

文件夹和文件之间的关系,非常适合使用组合模式来描述。文件夹既可以包含文件,也可以包含其他的文件夹。(具体代码见10.7);

需要注意的地方。
  • 组合模式不是父子关系。组合模式是一种HAS-A的关系,而不是IS-A的关系。
  • 对叶对象操作的一致性。组合模式除了要求组合对象和叶对象拥有同样的借口之外,还要求对叶对象的操作具有一致性。比如公司给全体员工发放过节费1000元,这个可以用组合模式。但是如果公司给今天过生日的员工发放1000元,就不能用组合模式。除非先把今天过生日的员工挑选出来。只有用一致的方式对待组合列表中每个叶对象的时候,才适合使用组合模式。
  • 双向映射关系。发放过节费的步骤是从公司-部门-小组-员工,本身是一个很好的组合模式的例子。但是要考虑的一种情况是,也许某些员工是多组织的。比如某个员工既属于架构组,又属于开发组,这时候对象之间的关系并不是严格意义上的层次结构。这种情况下,不适合用组合模式。
    这种情况下需要给父节点和子节点建立双向映射,我们可以引入中介者模式来管理这些对象。
  • 用职责链模式提高组合模式性能。在组合模式中,如果树的结构比较复杂,节点比较多,遍历的过程中,性能方面也许表现的不够理想。我们可以借助职责链模式,来避免遍历整棵树。
适合使用组合模式的场景:
  • 表示对象的部分-整体层次结构。
  • 客户希望统一对待树中的所有对象。

8.模板方法模式

概念和组成

模板方法模式是一种只需要使用继承就可以实现的模式。
模板方法模式由两部分组成,第一部分是抽象父类,第二部分是具体的实现子类。通常在抽象父类中封装了子类的算法框架,包括一些公共方法以及封装子类中所有方法的执行顺序。子类通过继承这个抽象类,也继承了整个算法框架,并且可以选择重写父类的方法。
假设我们有一些平行的子类,子类有一些相同的行为和不同的行为。如果相同和不同的行为混合在各个子类的实现中,说明相同的行为会重复出现。实际上,相同的行为可以被搬到另外一个单一的地方,模板方法就是为解决这个问题而生的。
在模板方法模式中,子类实现中相同的部分被上移到父类中,而将不同的部分留在子类来实现。

coffee or tea

接下来我们用一个例子来看一下这个模式:
假设泡一杯咖啡的流程是:把水煮沸-用沸水泡咖啡-把咖啡倒进杯子-加糖和牛奶
假设泡一壶茶的流程是:把水煮沸-用沸水浸泡茶叶-把茶水倒进杯子-加柠檬
我们可以看到泡咖啡和茶的不同点:

  • 原料不同:一个是咖啡,一个是茶,但我们可以把它们都抽象为饮料
  • 泡的方式不同:咖啡是冲泡,茶叶是浸泡,可以抽象为泡
  • 加的调料不同:一个是糖和牛奶,一个是柠檬,可以抽象为调料。
    经过抽象,步骤为:把水煮沸-用水泡饮料-把饮料倒进杯子-加调料,代码如下:
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(){   
//init方法为模板语法,封装子类的算法框架,
//它作为一个算法的模板,指导子类以何种顺序去执行那些方法。
  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();
//同样的,浸泡茶也用同样的方法即可。
抽象类,具体类,抽象方法,具体方法

模板语法严重依赖于抽象类来实现。
抽象类不可以被实例化,抽象类是用来被某些具体类继承的,抽象类可以用于向上转型,抽象类也可以表示为一种契约,继承了抽象类的所有的子类,都将拥有和抽象类一致的接口方法。
具体类可以被实例化。
抽象方法被声明在抽象类中,抽象方法并没有具体的实现过程。当子类继承了抽象类时,必须重写抽象类中的抽象方法。
每个子类中都有一些具体的实现方法,如果有相同的方法,也可以放在抽象类中,节省代码,以达到复用的效果,这种方法叫做具体方法。

js中没有抽象类的去诶单以及解决办法

缺点:js没有提供对抽象类的支持,并且在js中使用原型继承来模拟传统的类式继承时,并没有编译器帮助我们进行任何形式的检查,我们没法保证在子类中一定会重写父类的抽象方法。
解决方案:

  • 1.用鸭子类型来模拟接口检查,确保子类重写父类的抽象方法。但模拟接口检查会带来不必要的复杂性,而且要求程序员主动进行接口检查,这要求我们在业务代码中添加很多和业务逻辑无关的代码。
  • 2.让Beverage.prototype.brew等需要重写的方法直接抛出一个异常。这样如果忘记写Coffee.prototype.brew方法,我们至少可以在程序运行时看到一个错误。这种方法实现简单,付出代价少,但是我们得到错误信息提示的时间点靠后,不能在编写代码,创建对象时得到错误信息,只有当运行时才能得到错误信息。
钩子方法

通过模板方法模式,我们在父类中封装了子类的算法框架。这些算法框架在正常情况下适用于大多数的子类,但是如果有一些特殊的子类,比如我们封装的饮料的冲泡顺序:把水煮沸-用水泡饮料-把饮料倒进杯子-加调料。这个适用于冲泡茶和咖啡,但如果有一些客人喝咖啡不加调料,Beverage作为父类已经规定了冲泡的四个步骤,那么有什么方法可以让子类不受约束呢。

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

在这个例子中,我们把挂钩的名字定为customerWantsCondiment,接下来放入Beverage类,看看我们如何得到一杯不需要糖和牛奶的咖啡,代码如下:

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(){   
//init方法为模板语法,封装子类的算法框架,
//它作为一个算法的模板,指导子类以何种顺序去执行那些方法。
  this.boilWater();
  this.brew();
  this.pourInCup();
  if ( this.customerWantsCondiments() ){
    //如果挂钩返回true,则需要调料
    this.addCondiments();
  }
};
var CoffeeWithHook = function(){};
CoffeeWithHook.prototype = new Beverage();
CoffeeWithHook.prototype.brew = function(){
  console.log('用沸水冲泡咖啡');
};
CoffeeWithHook.prototype.pourInCup = function(){
  console.log('把咖啡倒进杯子');
};
CoffeeWithHook.prototype.addCondiments = function(){
  console.log('加糖和牛奶');
};
CoffeeWithHook.prototype.customerWantsCondiments = function(){
  return window.confirm( '请问需要调料吗?' );
};
var coffeeWithHook = new CoffeeWithHook();
coffeeWithHook.init();
好莱坞原则

好莱坞是演员的天堂,但是好莱坞也有很多找不到工作的新人,这些新人往往把简历投给演艺公司之后,就只能回家等电话。有时候演员等的不耐烦了,会给演艺公司打电话询问,演艺公司往往这样回答:‘不要来找我,我会给你打电话’,在程序中我们称之为好莱坞原则。

在好莱坞原则的指导下,我们允许底层组件将自己挂钩到高层组件中,而高层组件会决定什么时候,以何种方式来使用这些底层组件,高层组件对待底层组件的方式,跟演艺公司对待新人演员的方式一样,都是“别调用我们,我们会调用你”。
好莱坞原则的应用场景
  • 模板方法模式。当我们使用模板方法编写一个程序是,就意味着子类放弃了对自己的控制权,而改为由父类通知子类,哪些方法在什么时候以何种方式调用,子类只负责一些设计上的细节。
  • 发布订阅模式。发布者会把消息推送给订阅者,这取代了原先不断去fetch消息的形式。
  • 回调函数。在ajax异步请求中,由于不知道请求返回的具体时间,而通过轮询去判断是否返回数据显然是不理智的,所以我们通常会把接下来的操作放在回调函数中,传入发起ajax异步请求的函数中,当数据放回之后,这个回调函数才被执行,这也是好莱坞原则的一种体现。把需要的操作封装在一个函数里面,然后把主动权交给另外一个函数。至于回调函数什么时候调用,由另外一个函数控制。
js并没有提供真正的类式继承,继承是通过对象和对象之间的委托来实现的。那么在js中,在好莱坞原则的指导下,实现这样一个例子,只需要如下代码:
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 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 coffee = new Coffee();
coffee.init();
//茶按照同样的方法即可

模板方法模式是一个典型的通过封装变化提高系统扩展性的设计模式。在传统的面向对象语言中,一个运用了模板方法模式的程序中,子类的方法类和执行顺序都是不变的,所以我们把这部分逻辑抽象到父类的模板方法里面。而子类的方法具体是怎么实现则是可变的,所以我们把这部分变化的逻辑封装到子类中。通过增加新的子类,我们便能给系统增加新的功能,并不需要改动抽象父类以及其他子类,这也是符合开放-封闭原则的。
但在js中,我们很多时候不需要这样实现一个模板方法模式,高级函数才是更好的选择。


9.享元模式

概念

享元模式是一种用于性能优化的模式。它的核心是运用共享技术来有效支持大量细粒度的对象。如果系统中因为创建了大量类似的对象而导致内存占用过多,享元模式就很有用了。
在js中,浏览器特别是移动端的浏览器分配的内存并不算多,如何节省内存就成了一件很有意义的事情。
假设有个内衣工厂,目前的产品有50种男士内衣和50种女士内衣,为了推销产品,工厂决定生产一批塑料模特来穿山他们的内衣拍摄广告片。在不适用享元模式的情况下,我们需要50个男模特和50个女模特。
代码如下:

var Model = function( sex,underwear ) {
  this.sex = sex;
  this.underwear = underwear;
};
Model.prototype.takePhoto = function(){
  console.log(  this.sex + '....' + this.underwear);
};
for ( var i = 0; i <= 50; i++ ){
  var maleModel = new Model( 'male','underwear' +i );
  maleModel.takePhoto();
};
for ( var j = 0; j <= 50; j++ ){
  var femaleModel = new Model( 'female','underwear' +j );
  femaleModel.takePhoto();
};

在上面的例子中,要得到一张照片,每次都要传入sex和underwear参数,并且现在一共有50个男士内衣和50个女士内衣,所以一共产生了100个对象。如果生产10000种内衣,那这个程序可能会因为存在如此多的对象已经提前崩溃。

但是在使用享元模式的情况下,我们不需要创建100个对象,其实男女模特各只需要一个就可以了,他们可以分别穿上不同的衣服拍照。

代码如下:

var Model = function( sex ) {
  this.sex = sex;
};
Model.prototype.takePhoto = function(){
  console.log(this.sex + "....." + this.underwear);
};  
var maleModel = new Model( 'male' );
var femaleModel = new Model( 'female' );
for (var i = 0; i <= 50; i++){
  maleModel.underwear = 'underwear' + i;
  maleModel.takePhoto();
};
for (var j = 0; j <= 50; j++){
  femaleModel.underwear = 'underwear' + j;
  femaleModel.takePhoto();
};
内部状态与外部状态

享元模式要求将对象的属性划分为内部状态和外部状态(这里的状态通常指的是属性)。享元模式的目标是尽量减少共享对象的数量,对于如何划分内部状态和外部状态,下面的几条经验提供了一些指引:

  • 内部状态存储于对象内部
  • 内部状态可以被一些对象共享
  • 内部状态独立于具体的场景,通常不会改变
  • 外部状态取决于具体的场景,并且根据场景的变化而变化,外部状态不能被共享。
    我们可以把所有内部状态相同的对象都指定为同一个共享对象,而外部状态可以从对象身上剥离出来,并存储在外部。外部状态在必要时传入共享对象,组成一个完整的对象。
享元模式的通用结构

上面我们已经初步展示了享元模式的效果,但是还不是一个完整的享元模式,以上例子还存在两个问题

  • 我们通过构造函数new出了两个男女model对象,在其他系统中,也许不是一开始就需要所有的共享对象
  • 给model手动设置了underwear外部状态,在复杂的系统中,这不是一好的方式,因为外部状态可能会相当复杂,他们与共享对象的联系变得困难。

我们可以通过一个工厂函数来解决第一个问题,只有当某种共享对象被需要时才会创建。对于第二个问题,我们可以用一个管理器来记录对象相关的外部状态,使这些外部状态通过某个钩子和共享对象联系起来。

享元模式的适用性

缺点:会带来一些复杂性的问题,比如要多维护一个工厂对象和一个管理器,在大部分不必要使用享元模式的环境下,这些开销是可以避免的。
以下情况比较适合使用享元模式:

  • 一个程序中使用了大量相似的对象。
  • 由于使用了大量对象,造成很大的内存开销。
  • 对象的大多状态都可以变为外部状态。
  • 剥离出对象的外部状态之后,可以用相对较少的共享对象取代大量对象。
    在某些情况下,会存在没有内部状态或没有外部状态的对象。
    没有内部状态的享元,生产共享对象的工厂函数实际上就变成了一个单例工厂,虽然这个时候没有内部状态的区分,但还是有剥离外部状态的部分,我们依然倾向于称之为享元。
    没有外部状态的分离,即使使用了共享的技术,但并不是一个纯粹的享元模式。
对象池

对象池维护一个装载空闲对象的池子,如果需要对象的时候,不是直接new,而是从对象池里面获取。如果对象池里面没有空闲对象,则创建一个新的对象,当获取出的对象完成它的职责之后,再进入池子等待下次获取。
比如,我们人手一本js书籍,从节约的角度来讲,这不是很划算,因为大部分时间,这些书都被闲置在各自的书架上,所以我们一开始只买一本,或者是建一个小型图书馆,需要看书的从图书馆借,看完之后再还给图书馆。如果同时有三个人需要这本书,而现在图书馆里面只有2本,我们就会买上再去书店买一本放入图书馆。

对象池技术运用非常广发,HTTP连接池和数据库连接池就是其代表应用。在前端开发中,对象池使用最多的场景大概就是跟DOM有关的操作。很多时间和空间都消耗在了DOM节点上面,如何避免频繁的创建和删除DOM就成了一个有意义的话题。
对象池的实现

假设我们开发了一个地图应用,地图上经常出现一些标志地名的小气泡,我们称之为toolTip。在搜索我家附近地图的时候,页面出现两个小气泡,当再搜索附近的兰州拉面馆的时候,页面中出现了6个气泡。按照对象池技术,第二次搜索开始前,并不会把第一次创建的两个小气泡删除掉,而是把它们放进对象池。这样第二次搜索结果页面,我们只需要创建4个小气泡,而不是6个小气泡。
通用对象池的实现代码如下:

var objectPoolFactory = function( createObjFn ){
  //toolTip对象池
  var objectPool = [ ];
  return {
    create: function(){
    //如果对象池为空,调用传入的回调函数,创建对象,否则取出对象池的第一个对象。
      var obj = objectPool.length === 0 ? createObjFn.apply( this, arguments) : objctPool.shift();
      return obj;
    },
    recover: function( obj ){
      //对象池回收对象
      objectPool.push( obj );
    }
  }
}

现在利用objectPoolFactory来创建一个装载一些iframe的对象池。

var iframeFactory = objectPoolFactory( function(){
  var iframe = document.createElement( 'iframe' );
  document.body.appendChild( iframe );
  iframe.onlaod = function(){
    iframe.onload = null; //防止iframe重复加载的bug
    iframeFactory.recover( iframe );  //iframe加载完成之后回收节点 
  }
  return iframe;
});
var iframe1 = iframeFactory.create();
iframe1.src = "http://baidu.com";
var iframe2 = iframeFactory.create();
iframe2.src = "http://qq.com";
setTimeout(function(){
  var iframe3 = iframeFactory.create();
  iframe3.src = "http://163.com";
},3000)
对象池是另外一种性能优化方案,它跟享元模式有一些相似之处,但没有分离内部状态和外部状态的这个过程。
享元模式是为了解决性能问题而生的模式,这跟大部分模式的诞生原因都不一样。在一个存在大量相似对象的系统中,享元模式可以很好的解决大量对象带来的性能问题。

10.职责链模式

概念

使多个对象都有机会处理请求,从而避免请求的发送者和接收者之间的耦合关系,将这些对象连成一条链,并沿着这条链传递该请求,直到一个对象处理它为止。
职责链在现实生活中的小场景:

  • 早上挤上公交车之后,由于公交车上人很多,经常找不到售票员在哪,所以只好把硬币往前递。除非你运气够好,站在你前面的第一个人就是售票员,否则,你的硬币通常要经过N个人传递,才能最终到达售票员手里。
  • 中学时代的期末考试,如果你平时不太老实,就会被安排在第一个座位。遇到不会的题目,就把题目编号写在纸条上往后传,坐在后面的同学如果也不会的话,就会把纸条继续传递给它后面的人。
从这两个例子,我们很容易看到职责链模式的优点:请求的发送者只需知道链中的第一个节点,从而弱化了发送者和一组接收者之间的强联系。如果不使用职责链模式,那么在公交车上,就得先搞清楚谁是售票员,才能把硬币递给他。
实际开发中的职责链模式

假设我们负责一个售卖手机的电商网站,经过分别交纳500元定金和200元定金的两轮预定之后,现在已经到了正式购买的阶段。公司针对支付过定金的用户有一定的优惠政策。在正式购买之后,已经支付过500元定金的用户会收到100元的优惠券,200元定金的用户会收到50元优惠券,而之前没有支付定金的用户只能进入到普通购买模式,也就是没有优惠券,且在库存有限的情况下不一定买的到。
现在我们采用职责链模式来实现这个功能,先把500元订单,200元订单,普通购买分为3个函数,我们约定,如果某个节点不能处理请求,则返回一个特定的字符串“nextSuccessor”来表示请求需要继续往后传递。
代码如下:

var order500 = function( orderType, pay, stock ) {
  if ( orderType == 1 && pay == true ) {
    console.log( "500元定金预购,得到100元优惠券" );
  } else {
     //不知道下一个节点是谁,反正把请求往后面传递
    return 'nextSuccessor'; 
  }
};
var order200 = function( orderType, pay, stock ) {
  if ( orderType == 2 && pay == true ) {
    console.log( "200元定金预购,得到50元优惠券" );
  } else {
     //不知道下一个节点是谁,反正把请求往后面传递
    return 'nextSuccessor'; 
  }
};
var orderNormal = function( orderType, pay, stock ) {
  if ( stock > 0 ) {
    console.log( "普通购买,无优惠券" );
  } else {
    console.log( "手机库存不足" ); 
  }
};
//接下来把函数包装进职责链节点,我们定义一个构造函数Chain,
//在new Chain的时候传递的参数即为被包装的函数
//同时它还拥有一个实例属性this.successor,表示在链中的下一个节点。
//此外,Chain的prototype中还有两个函数,他们的作用如下:
//Chain.prototype.setNextSuccessor 指定在链中的下一个节点
//Chain.prototype.passRequest 传递请求给某个节点
var Chain = function( fn ){
  this.fn = fn;
  this.successor = null;
};
Chain.prototype.setNextSuccessor = function( successor ){
  return this.successor = successor;
};
Chain.prototype.passRequest = function(){
  var ret = this.fn.apply( this,arguments );
  if ( ret === 'nextSuccessor' ){
    return this.successor && this.successor.passRequest.apply( this.successor,arguments )
  }
  return ret;
};
//把三个订单函数分别包装成职责链节点:
var chainOrder500 = new Chain( order500 );
var chainOrder200 = new Chain( order200 );
var chainOrderNormal = new Chain( orderNormal );
//指定节点在职责链中的顺序
chainOrder500.setNextSuccessor( chainOrder200 );
chainOrder200.setNextSuccessor( chainOrderNormal );
//把请求传递给第一个节点
ChainOrder500.passRequest( 1,true,500 );
//输出:500元定金预购,得到100元优惠券
ChainOrder500.passRequest( 2,true,500 );
//输出:200元定金预购,得到50元优惠券
ChainOrder500.passRequest( 3,true,500 );
//输出:普通购买,无优惠券
ChainOrder500.passRequest( 1,false,0 );
//输出:手机库存不足
异步的职责链

在上面的例子中,每个节点函数都同步返回一个特定的值”nextSuccessor“,来表示是否把值传递给下一个节点。而在现实开发中,我们经常遇到一些异步问题,比如我们要在节点函数中发送一个ajax请求,异步返回的结果才能决定是否继续值职责链中passRequest.
这个时候让节点函数同步返回”nextSuccessor“已经没有意义了,所以要给Chain再增加一个原型方法Chain.prototype.next,表示手动传递请求给职责链中的下一个节点。

Chain.prototype.next = function(){
  return this.successor && this.successor.passRequest.apply( this.successor,arguments )
}

我们来看一个异步职责链的例子

var fn1 = new Chain( function(){
  console.log(1);
  return 'nextSuccessor';
});
var fn2 = new Chain( function(){
  console.log(2);
  var self = this;
  setTimeout( function(){
    self.next();
  },1000);
});
var fn3 = new Chain( function(){
  console.log(3);
});
fn1.setNextSuccessor( fn2 ).setNextSuccussor( fn3 );
fn1.passRequest();
职责链模式的优缺点

优点:

  • 解耦了请求发送者和N个接收者之间的关系,由于不知道链中的那个节点可以处理你发出的请求,所以只需要把请求传递给第一个节点即可。
  • 链中的节点对象可以灵活地拆分重组。
  • 可以手动指定起始节点,请求并不是非得从链中的第一个节点开始。
    缺点:
  • 不能保证请求一定会被链中的某一个节点处理。针对请求不能被链中的任意一个节点处理的情况,我们可以在链尾增加一个保底的接收者节点来处理这种即将离开链尾的请求。
  • 使程序中多了一些节点对象,可能在某一次请求传递过程中,大部分节点并没有起到实质性的作用,他们的作用仅仅是将请求传递下去,从性能方面考虑,我们要避免过长的职责链带来的性能损耗。
用AOP实现职责链

之前的职责链实现中,我们利用一个Chain类来把普通函数包装成职责链的节点。其实利用js的函数式特性,有一种更加方便的方法来创建职责链。
下面我们来改写一下Function.prototype.after函数,使得第一个函数返回‘nextSucessor’时,请求继续传递给下一个函数。
代码如下:

Function.prototype.after = function( fn ){
  var self = this;
  return function(){
    var ret = self.apply( this,arguments );
    if ( ret === "nextSuccessor" ) {
      return fn.apply( this,arguments );
    }
    return ret;
  }
};
var order = order500.after( order200 ).after( orderNormal );
order( 1,true,500 );
//输出: 500元定金预购,得到100元优惠券
order( 2,true,500 );
//输出: 200元定金预购,得到50元优惠券
order( 1,false,500 );
//输出: 普通购买,无优惠券
注意:用AOP来实现职责链简单又巧妙,但这种把函数叠在一起的方式,同时叠加了函数的作用域,如果链条太长的话,也会对性能有较大影响。
用职责链获取文件上传对象

在迭代器模式中,饿哦们创建了一个迭代器来迭代获取何时的文件上传对象,其实用职责链更简单,我们完全不用创建多余的迭代器,代码如下:

var getActiveUploadObj = function(){
  try{
    return new ActiveXObject( 'TXFTNActiveX.FTNUpload'); //ie控件上传
  } catch(e) {
    return 'nextSuccessor';
  }
}; 
var getFlashUploadObj = function(){
  if ( supportFlash() ) {
    var str = "";
    return $(str).appendTo( $('body') );
  };
  return 'nextSuccessor';
}; 
var getFormUploadObj = function(){
  return $( "
" ) }; var getUploadObj = getActiveUploadObj.after( getFlashUploadObj ).after( 'getFormUploadObj' ); console.log( getUploadObj() );

11.中介者模式

在程序世界里,程序是由大大小小的单一的对象醉成的,所有这些对象将按照某种关系和规则来通信。在程序里,一个对象有可能会和其他10个对象打交道,所以它会保持对10个对象的引用。当程序的规模扩大之后,对象之间的引用越多,他们的关系会越来越复杂,难免会形成网状的交叉引用。当我们改变一个对象时,很可能需要通知所有引用到它的对象,这样,即使是很小的一点改动都需要小心翼翼。
面向对象设计,鼓励将行为分布到各个对象中,把对象划分为更小的粒度,增强对象的可复用性,但是由于这些细粒度对象之间的联系激增,又可能反过来降低它们的复用性。

中介者的作用就是解除对象与对象之间的强耦合关系。增加一个中介者对象之后,所有的相关对象都通过中介者来通信,而不是相互引用,所以当一个对象发生变化之后,只需要通知中介者即可。中介者使各对象之间耦合松散,而且可以独立的改变他们之间的交互。中介者模式使网状的多对多的关系变成了简单的一对多的关系。
中介者模式的例子-泡泡堂游戏

首先定义player构造函数和player对象的原型方法,在player对象的这些原型方法中,不再负责具体的执行逻辑,而是把操作权限转交给中介者对象,我们把中介者对象命名为playerDirector
代码如下

function Player( name, teamColor ){
  this.name = name;  //角色名称
  this.teamColor = teamColor; //队伍颜色
  this.state = 'alive';  //玩家生存状态
};
Player.prototype.win = function(){
  console.log( this.name + 'win' );
};
Player.prototype.lose = function(){
  console.log( this.name + 'lose' );
};
//玩家死亡
Player.prototype.die = functioni(){
  this.state = 'dead';
  //给中介者发送信息,玩家死亡
  PlayerDirector.ReceiveMesage(  'playerDead', this);
};
//移除玩家
Player.prototype.remove = function(){  
  //给中介者发送消息,移除一个玩家
  playerDirector.ReceiveMessage( 'removePlayer',this );
};
//玩家换队
Player.prototype.changeTeam = function(){
  //给中介者发送消息,玩家换队
  playerDirector.ReceiveMessage( 'changeTeam', this, color );
};
//创建玩家的工厂函数
var playerFactory = function( name, teamColor ){
  var newPlayer = new Player( name, teamColor );  //创建一个玩家
  playerDirector.ReceiveMessage( 'addPlayer', newPlayer );
  return newPlayer;
};
//最后我们来实现中介者playerDirector对象,一般有以下两种方式
//1.利用发布-订阅模式,将playerDirector作为订阅者,各player为发布者。
//2.在playerDirector中开放一些接受消息的接口,各player可以直接调用接口来给playerDirector发送消息
//在这里我们使用第二种方式
var playerDirector = ( function(){
  var players = {},  //保存所有玩家
        operations = {}; //中介者可以执行的操作
  operations.addPlayer = function( player ){
    var teamColor = player.teamColor;  //玩家的队伍颜色
    //如果该颜色的玩家还没有成立队伍,则新成立一个队伍
    players[ teamColor ] = players[ teamColor ] || [ ];
    players[ teamColor ].push( player ); //添加玩家队伍
  }; 
  //移除一个玩家
  operations.removePlayer = function( player ) {
    var teamColor = player.teamColor; //玩家的队伍颜色
    teamPlayers = players[ teamColor ] || [];  //该队伍所有成员
    for ( var i = teamPlayers.length-1; i >=0; i-- ){
      if( teamPlayers[ i ] === player ){
        teamPlayers.splice(  i,1 );
      }
    };
  };
  //玩家换队
  operations.changeTeam = function( player,newTeamColor ){
    operations.removePlayer( player ); //玩家换队
    player.teamColor = newTeamColor; //从原队伍中删除
    operations.addPlayer( player );  //增加到新队伍中
  };
  operations.playerDead = function( player ){
   //玩家死亡
   var teamColor = player.teamColor,
        teamPlayers  = players[ teamColor ];
   var all_dead = true;
   for( var i = 0, player; player = teamPlayers[ i++ ] ){
      if( player.state !== 'dead' ){
        all_dead = false;
        break;
      };
    };
    if( all_dead === true ){
      //全部死亡
      for( var i = 0; player; player == teamPlayers[ i++ ] ){
        player.lose(); //本队所有玩家lose
      };
      for( var color in players ){
        if( color !== teamColor ){
          var teamPlayers = players[ color ]; //其他队伍的玩家
          for( var i =0,player; player = teamPlayers[ i++ ]; ){
            player.win(); //其他队伍所有玩家win
          };
        };
      }
    }
  };
  var ReceiveMessage = function(){
    //arguments的第一个参数为消息名称
    var message = Array.prototype.shift.call( arguments );
    operations[ message ].apply( this, arguments );
  };
  return {
    ReceiveMessage: ReceiveMessage
  }
})();
var Player1 = playerFactory( '皮蛋', 'red' );
var Player2 = playerFactory( '小乖', 'red' );
var Player3 = playerFactory( '宝宝', 'red' );
//蓝队
var Player4 = playerFactory( '小强', 'blue' );
var Player5 = playerFactory( '小黑', 'blue' );
var Player6 = playerFactory( '小白', 'blue' );
player1.die();
player2.die();
player3.die();
//假设皮蛋掉线
player1.remove();
//假设皮蛋从红队叛变到蓝队
player1.changeTeam( 'blue' );
中介者模式的缺点
  • 系统中会新增一个中介者对象,因此对象之间交互的复杂性转移成了中介者对象的复杂性,使得中介者对象通常是巨大的。中介者对象往往是一个难以维护的对象
  • 中介者模式可以方便的对模块或者对象解耦,但对象之间并非一定需要解耦。一般来说,如果对象之间的复杂耦合确实导致调用和维护出现了困难,而且这些耦合度随项目的变化呈指数增长曲线,那我们可以考虑用中介者模式来重构代码。

12.装饰者模式

概念

装饰者模式可以动态的给某个对象添加一些额外的职责,而不会影响从这个类中派生的其他对象。
在传统的面向对象的语言中,给对象添加功能常常使用继承的方式,但是继承不是很灵活,会带来许多的问题:

  • 导致超类和子类之间存在强耦合,当超类改变时,子类也会随之改变。
  • 在继承方式中,超类的内部细节对于子类来说是可见的,继承常常被认为破坏了封装性。
  • 在完成一些功能复用的过程中,有可能创建出大量的子类,使子类的数量呈爆炸性增长。比如,现在有四种型号的自行车,我们为每种自行车都定义一个单独的类。现在要给每种自行车都装上前灯,尾灯和铃铛这三种配件。如果使用继承的方式为每种自行车创建子类,则需要4*3 = 12个子类。但是如果把前灯,尾灯,铃铛这些对象动态组合到自行车上面,则只需要额外增加三个类。
    这种给对象动态添加职责的方式称为装饰者模式。跟继承相比,装饰者是一种更灵活的方式,是一种‘即用即付’的方式。
模拟传统面向兑现语言的装饰者模式

假设我们在编写一个飞机大战的游戏,随着经验值得增加,我们操作的飞机对象可以升级为更厉害的飞机,一开始这些飞机只能发射普通的子弹,升级到第二级的时候可以发射导弹,升到三级时可以发射原子弹。
代码如下

var Plane = function(){};
Plane.prototype.fire = function(){
  console.log('发射普通子弹');
};
//接下来增加两个装饰类,分别为导弹和原子弹
var MissileDecorator = function( plane ){
  this.plane = plane;
};
MissileDecorator.prototype.fire = function( ){
  this.plane.fire();
  console.log( '发射导弹' );
};
var AtomDecorator = function( plane ){
  this.plane = plane;
};
AtomDecorator.prototype.fire = function(){
  this.plane.fire();
  console.log( '发射原子弹' );
};
var plane = new Plane();
plane = new MissileDecorator( plane );
plane = new AtomDecorator( plane );
plane.fire();
//分别输出:发射普通子弹,发射导弹,发射原子弹。

导弹类和原子弹类的构造函数都接受参数plane对象,并保存这个参数,在他们的fire方法中,除了执行自身的操作之外,还调用plane对象的fire方法。

这种给对象动态添加职责的方式,并没有真正的改变对象本身,而是把对象放入另一个对象中间,这些对象以一条链的方式进行引用,形成一个聚合对象。这些对象都拥有相同的接口(fire方法),当请求到达链中的某个对象时,这个对象会执行自身的操作,随后把请求转发给链中的下一个对象。

因为装饰者对象和它所装饰的对象拥有一致的接口,所以它们对使用该对象的客户来说是透明的,被装饰的对象并不需要了解它曾经被装饰过,这种透明性使得我们可以递归地嵌套任意多个装饰者对象。

装饰者模式将一个对象嵌入另一个对象之中,实际上相当于这个对象被另一个对象包装起来,形成一个包装链。请求随着这条链依次传递到所有的对象,每个对象都有处理这条请求的机会。
js中的装饰者
var plane = {
  fire: function(){
    console.log( "发射普通子弹" );
  };
};
var missileDecorator = function(){
  console.log( " 发射导弹 " );
};
var atomDecorator = function(){
  console.log( "发射原子弹" );
};
//通过保存原引用的方式改写某个函数。
var fire1 = plane.fire;
plane.fire = function(){
  fire1();
  missileDecorator();
};
var fire2 = plane.fire;
plane.fire = function(){
  fire2();
  atomDecorator();
};
plane.fire();

通过保存原引用方式改写某个函数的另一个例子

//假设我们想给window对象绑定onload事件,但是又不确定这个事件是不是已经被其他人绑定过
//为了避免覆盖掉之前的window.onload函数中的行为,我们一般会先保存好原先的window.onload,把它放入到新的window.onload里执行:
window.onload = function(){
  alert(1);
};
var _onlad = window.onload || function(){};
window.onload = function(){
  _onlad();
  alert(2);
}

上面的代码虽然符合开放-封闭原则,也没有直接修改原函数,但是这种方式存在以下两个问题:

  • 必须维护_onload这个中间变量,虽然看起来不起眼,但如果函数的装饰链较长,或者需要装饰的函数变多,这些中间变量的数量就会越来越多。
  • 会遇到this被劫持的问题,在window.onload这个例子里面没有这个问题,是因为调用普通函数_onlad时,this指向的是window,跟调用window.onload时一样。(函数作为对象的方法调用时,this指向该对象,所以此处this指向的是window)。
用AOP装饰函数

代码如下:

//首先给出Function.prototype.before和Function.prototype.after方法
Function.prototype.before = function( beforeFn ){
  //  保存原函数的引用
  var _self = this;
  //返回包含了原函数和新函数的代理函数
  return function(){
    //执行新函数并保证this不会被劫持
    //新函数接受的参数也会被原封不动的传给原函数,新函数在原函数之前执行
    beforeFn.apply(this,arguments);
    return _self.apply(this, arguments);
    //执行原函数并返回原函数的执行结果,并保证this不会被劫持
  }
};
Function.prototype.after= function( afterFn){
  var _self = this;
  return function(){
    var ret = _self.apply( this,arguments );
    afterFn.apply(this,arguments);
    return ret;
  }
};
//改写之前的window.onload
window.onload = function(){
  alert(1);
};
window.onload = (window.onlad || function(){}).after(function(){
  alert(2);
}).after(function(){
  alert(3);
})

上面AOP实现是在Function.prototype上添加before和after方法,很多人不喜欢这种污染原型的方式,那我们可以做一些变通,把原函数和新函数都作为参数传入after和before方法
代码如下:

var before = function( fn,beforefn ){
  return function(){
    beforefn.apply( this,agruments );
    return fn.apply( this,arguments );
  }
};
var a = before( 
  function(){alert(2)}; 
  function(){alert(3)}; 
);
a();
AOP的应用实例

用AOP装饰函数的技巧在实际开发中非常有用。不论是业务代码的编写还是在框架层面,我们都可以把行为职责分为粒度更细的函数,随后通过装饰把它们合并到一起,这有助于我们编写一个松耦合和高复用性的系统。
例如:

  • 数据统计上报。分离业务代码和数据统计代码,无论在什么语言中,都是AOP的经典应用之一。比如在项目开发的结尾阶段,我们难免要加上很多统计数据的代码,这些过程可能让我们被迫改动早已经封装好的函数。
  • 插件式的表单验证。
    装饰者模式和代理模式结构上看起来非常相像,这两种模式都描述了怎样为对象提供一定程度上的间接引用,他们的实现部分保留了对另外一个对象的引用,并且像那个对象发送请求。
代理模式和装饰者模式最重要的区别在于它们的意图和设计目的。代理模式的目的是当访问本体不方便或者不符合需要时,为这个本体提供一个替代者。本体定义了关键功能,而代理提供或拒绝对它的访问,或者在访问本体之前做一些额外的事情。装饰者模式的作用是为对象动态加入行为。换句话说,代理模式强调一种关系,这种关系可以静态的表达,也就是说这种关系一开始就能被确定。装饰者模式用于一开始不能确定对象的全部功能时。代理模式通常只有一层代理-本体的引用,而装饰者模式经常会形成一条长长的装饰链。装饰者模式是实实在在的为对象添加职责和行为,而代理做的事情还是跟本体一样。

13.状态模式

概念

允许一个对象在其内部状态改变时改变它的行为,对象看起来似乎修改了它的类。

状态模式的关键是区分事物内部的状态,事物内部状态的改变往往会带来事物的行为改变。

假设有一个电灯,电灯上面只有一个开关。当电灯开着的时候,此时按下开关,电灯会切换到关闭状态,再按一次开关,电灯又将被打开。同一个开关按钮,在不同的状态下,表现出来的行为是不一样的。
代码如下:

var Light = function(){
  this.offLightState  = new OffLightState( this );  //持有对象状态的引用
  this.weakLightState = new WeakLightState( this );
  this.strongLightState = new StrongLightState( this );
  this.superStrongLightState = new SuperStrongLightState( this );
  this.button = null;
};
Light.prototype.init = function(){
  var button = document.createElement( 'button' ),
        self = this;
  this.button = document.body.appendChild( button );
  this.button.innerHTML = '开关';
  this.currState = this.offLightState; //设置默认初始状态
  this.button.onclick = function(){
    //定义用户的请求动作
    self.currState.buttonWasPressed();
  }
};
var State = function(){};
State.prototype.buttonWasPressed = function(){
  throw new Error( '父类的buttonWasPressed方法必须被重写' );
};
var SuperStrongLightState = function( light ){
  this.light = light;
};
SuperStrongLightState.prototype = new State();  //继承抽象父类
SuperStrongLightState.prototype.buttonWasPressd = function(){
  //重写buttonWasPressed
  console.log( '关灯' );
  this.light.setState( this.light.offLightState ); 
}
状态模式的优缺点

优点:

  • 状态模式定义了状态与行为之间的关系,并将它们封装在一个类里。通过增加新的状态类,很容易增加新的状态和转换。
  • 避免context无限膨胀,状态切换的逻辑被分布在状态类中,也去掉了context中原本过多的条件分支。
  • 用对象代替字符串来记录当前状态,使得状态的切换更加一目了然。
  • context中的请求动作和状态类中封装的行为可以非常容易的独立变化而且互不影响。
    缺点:
  • 会在系统中定义许多状态类,系统中会因此增加不少对象。
  • 由于逻辑分散在状态类里面,虽然避开了不受欢迎的条件分支语句,但也造成了逻辑分散的问题,我们无法在一个地方就看出整个状态机的逻辑。
状态模式中的性能优化点
  • 有两种选择来管理state对象的创建和销毁。第一种是仅当state对象被需要时才创建并随后销毁,另一种是一开始就创建好所有的状态对象,并且始终不销毁他们。当state对象比较庞大时,建议用第一种,如果状态改变很频繁,最好用第二种方式。
  • 在本章的例子中,我们为context对象创建了一组state对象,实际上这些state对象之间是可以共享的,各context对象可以共享一个state对象,这也是享元模式的应用场景之一。
状态模式和策略模式的关系

相同点:
他们都有一个上下文,一些策略或者状态类,上下文把请求委托给这些类来执行。
区别:
策略模式中的各个策略类之间是平等又平行的,他们之间没有任何联系。
状态模式中,状态和状态对应的行为早已被封装好,状态之间的切换也早已被对顶完成,改变行为这件事情发生在状态模式内部,客户并不需要了解这些细节。

js中的状态机
var Light = function(){
  this.currState = FSM.off ; //设置当前状态
  this.button = null;
};
Light.prototype.init = function(){
  var button = document.createElement( 'button' ),
        self = this;
  button.innerHTML = '已关灯';
  this.button = document.body.appendChild( button );
  this.button.onclick = function(){
    self.currState.buttonWasPressed.call( self ); //把请求委托给FSM状态机
  };
};
var FSM = {
  off:{
    buttonWasPressed: function(){
      console.log( '关灯' );
      this.button.innerHTML = '下一次按我是开灯';
      this.currState = FSM.on;
    }
  },
  on:{
      buttonWasPressed: function(){
        console.log( '开灯' );
        this.button.innerHTML = '下一次按我是关灯';
        this.currState = FSM.off;
      }
    }
  }
};
var light = new Light();
light.init();
表驱动的有限状态机

还有另外一种实现状态机的方法,这种方法的核心是基于表驱动的。我们可以在表中很清楚的看到下一个状态是由当前状态和行为共同决定的。这样一来,我们就可以在表中查找状态,而不必定义很多条件分支,
git上正好有一个对应的库实现,通过这个库,可以方便的创建出fsm:
代码如下:

var fsm = StateMachine.create({
  initial: 'off',
  events: [
    {name: 'buttonWasPressed', from: 'off', to: 'on'},
    {name: 'buttonWasPressed', from: 'on', to: 'off'}
  ],
  callbacks: {
    onBunttonWasPressed: function( event,from,to ){
      console.log( arguments );
    }
  },
  error: function( eventName, from, to, args, errorCode, errorMessage ){
    //从一种状态师徒切换到一种不可能到达的状态的时候
    console.log( arguments ); 
  }
});
button.onclick = function(){
  fsm.buttonWasPressed();
}

git地址: http://github.com/jakesgordon/javascript-state-machine


14.适配器模式

适配器的作用是解决两个软件实体间的接口不兼容的问题。在使用适配器模式之后,原本由于接口不兼容而不能工作的两个软件实体可以一起工作。

回顾我们之前的一个多态的例子,当我们向gooleMap和baiduMap都发出‘显示’请求时,googleMap和baiduMap分别以各自的方式在页面中展示了地图:

var googleMap = {
  show: function(){
    console.log( '开始渲染谷歌地图' );
  }
};
var baiduMap = {
  show: function(){
    console.log( '开始渲染百度地图' );
  }
};
var renderMap = function( map ){
  if ( map.show instanceof Function ){
    map.show();
  }
};
renderMap( googleMap );
renderMap( baiduMap );

这段程序得以顺利运行的关键是googleMap和baiduMap提供了一致的show方法,但第三方的接口方法并不在我们自己的控制范围之内,假如baiduMap提供的显示地图的方法不叫show而叫display呢?
baiduMap这个对象来源于第三方,正常情况下我们都不应该去改动它。此时我们可以添加baiduMapAdapter来解决问题:

var googleMap = {
  show: function(){
    console.log( '开始渲染谷歌地图' );
  }
};
var baiduMap = {
  show: function(){
    console.log( '开始渲染百度地图' );
  }
};
var baiduMapAdapter = {
  show: function(){
    return baiduMap.display();
  }
};
var renderMap = function( map ){
  if ( map.show instanceof Function ){
    map.show();
  }
};
renderMap( googleMap );
renderMap( baiduMapAdapter );

再看看另外一个例子。假设我们正在编写一个渲染广东省地图的页面。目前从第三方资源里获得了广东省的所有城市以及它们所对应的id,并且成功渲染到页面中:

var getGuangdongCity = function(){
   var guangdongCity = [
      {name: 'shenzhen', id:11},
      {name: 'guangzhou', id:12}
   ];
  return guangdongCity;
};
var render = function( fn ) {
  console.log( '开始渲染广东省地图' );
  document.write( JSON.stringify( fn() ) );
};
render( getGuangdongCity );

利用这些数据,我们编写完成了整个页面,并且稳定运行了一段时间。但后来发现这些数据不太可靠,里面还缺少很多城市。于是我们又在网上找到了另外一些数据资源,这次的数据更加全面,但是数据结构和现在运行的不一致,新结构如下:

var guangdongCity = {
  shenzhen:11,
  guangzhou:12,
  zhuhai: 13
}

除了大动干戈的改写渲染压敏的前端代码之外,另一种更轻便的解决方式就是新增一个数据格式转换的适配器:

var getGuangdongCity = function(){
   var guangdongCity = [
      {name: 'shenzhen', id:11},
      {name: 'guangzhou', id:12}
   ];
  return guangdongCity;
};
var render = function( fn ) {
  console.log( '开始渲染广东省地图' );
  document.write( JSON.stringify( fn() ) );
};
var addressAdapter = function( oldAddressfn ){
  var address = {},
        oldAddress = oldAddressfn();
  for( var i = 0,c; c = oldAddress[ i++ ]; ){
    address[ c.name ] = c.id
  }
  return function(){
    return address;
  }
};
render( addressAdapter(getGuangdongCity) );
适配器模式是一种相对简单的模式。在本书提到的设计模式中,有一些模式跟适配器模式的结构非常相似,比如装饰者模式,代理模式和外观模式,这几种模式都属于‘包装模式’,都是由一个对象来包装另一个对象。区别他们的关键仍然是模式的意图:
  • 适配器模式主要是用来解决两个已有接口之间不匹配的问题,他不考虑这些接口是怎样实现的,也不考虑他们将来可能会如何演化。适配器模式不需要改变已有的接口,就能够使他们协同工作。
  • 装饰者模式和代理模式也不会改变原有对象的接口,但装饰者模式的作用是为了给对象增加功能。装饰者模式常常形成一条长的装饰链,而适配器模式通常纸包装一次。代理模式是为了控制对对象的访问,通常也只包装一次。
  • 外观模式的作用倒是和适配器比较相似,有人把外观模式看成一组对象的适配器,但外观模式最显著的特点是定义了一个新的接口。

你可能感兴趣的:(第二部分: 14种策略模式(下))