该系列参考自 《JavaScript 设计模式》

以及 汤姆大叔的博文 深入理解 JavaScript 系列

前言

  • 模板方法模式
  • 观察者模式
  • 状态模式
  • 策略模式
  • 职责链模式
  • 命令模式
  • 访问者模式
  • 中介者模式
  • 备忘录模式
  • 迭代器模式
  • 解释器模式

一. 模板方法模式

父类中定义一组算法操作骨架,而将一些实现步骤延伸到子类中,使得子类在不改变父类算法结构的同时可重新定义算法某些实现步骤。

模板方法模式主要由两部分组成:

  • 抽象父类
  • 具体实现子类

演示

这里我们直接拿网上最常用的例子来举例说明

Coffee or Tea

  • 把水煮沸
  • 用沸水浸泡茶叶
  • 把茶水倒进杯子
  • 加柠檬
/* 抽象父类:饮料 */
var Beverage = function() {};
// (1) 把水煮沸
Beverage.prototype.boilWater = function() {
  console.log('把水煮沸');
};
// (2) 沸水浸泡
Beverage.prototype.brew = function() {
  throw new Error('子类必须重写brew方法');
};
// (3) 倒进杯子
Beverage.prototype.pourInCup = function() {
  throw new Error('子类必须重写pourInCup方法');
};
// (4) 加调料
Beverage.prototype.addCondiments = function() {
  throw new Error('子类必须重写addCondiments方法');
};

/* 模板方法 */
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();

通过模板方法模式,在父类中封装了子类的算法框架。这些算法框架在正常状态下是适用大多数子类的,但也会出现“个性”子类。

如上述流程,加调料是可选的。

钩子方法可以解决这个问题,放置钩子是隔离变化的一种常见手段。

/* 添加钩子方法 */
Beverage.prototype.customerWantsCondiments = function() {
  return true;
};
Beverage.prototype.init = function() {
  this.boilWater();
  this.brew();
  this.pourInCup();
  if (this.customerWantsCondiments()) {
    this.addCondiments();
  }
};

/* 实现子类 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('加牛奶');
};
Tea.prototype.customerWantsCondiments = function() {
  return window.confirm('需要添加调料吗?');
};
var tea = new Tea();
tea.init();

二. 观察者模式

又被称为发布订阅模式,或者消息机制。定义了一种依赖关系,解决了主体对象和观察者之间功能的耦合

其实他就是定义了一种一对多的关系,让多个观察者对象同时监听某一个主体对象,这个主体对象的状态发生变化时,就会通知所有的观察者对象,使得他们能够自动更新自己。

观察者对象的好处:

  • 支持简单的广播通信,自动通知所有已经订阅过的对象。
  • 页面载入后目标对象很容易与观察者存在一种动态关联,增加了灵活性。
  • 目标对象与观察者之间的抽象耦合关系能够单独扩展以及重用。

演示

js 通过对观察者模式的实现是通过回调函数的方式

我们来先定义一个 pubsub 对象,其内部包含了 3 个方法:订阅、退订、发布

// 创建一个观察者
var Observe = (function() {
  var _message = {};
  return {
    register: function(type, fn) {
      //注册信息接口
      if (typeof _message[type] === 'undefined') {
        _message[type] = [fn];
      } else {
        _message[type].push(fn);
      }
    },
    fire: function(type, args) {
      //发布消息接口
      if (!_message[type]) return;
      //消息信息
      var event = {
          type: type,
          args: args || {}
        },
        i = 0,
        len = _message[type].length;
      for (; i < len; i++) {
        //一次执行注册过的方法
        _message[type][i].call(this, event);
      }
    },
    remove: function(type, fn) {
      //移除消息接口
      if (_message[type] instanceof Array) {
        var i = _message[type].length - 1;
        for (; i > -0; i--) {
          _message[type][i] === fn && _message[type].splice(i, 1);
        }
      }
    }
  };
})();

Observe.register('test', function(e) {
  console.log(e.type, e.args.msg);
});
Observe.register('test', function(e) {
  console.log(e.type, 'Liusixin');
});

Observe.fire('test', {
  msg: 'test test'
});

三. 状态模式

当一个对象内部状态发生变化的时候,会导致起行为变化,看起来就像改变了对象

状态模式定义了一个对象,这个对象可以通过管理其内部状态从而是其行为发生变化。状态模式是一个非常出色的设计模式,主要由两个角色构成

  • 环境类:拥有一个状态成员,可以修改其状态并做出反应
  • 状态类:表示一种状态,包含相应的处理方法

演示

一个简单的例子,我们可以将不同的判断结构封装在一个状态对象内,然后该状态对象返回一个可被调用的状态方法,用于调用对象内部某个方法。

var ResultState = (function() {
  var States = {
    state0: function() {
      console.log('这是第一种结果');
    },
    state1: function() {
      console.log('这是第二种结果');
    },
    state2: function() {
      console.log('这是第三种结果');
    },
    state3: function() {
      console.log('这是第四种结果');
    }
  };
  //获取某一种状态并执行相应的方法
  function show(result) {
    States['state' + result] && States['state' + result]();
  }

  return {
    show: show
  };
})();

ResultState.show(4);

上面代码只是一个雏形,对于状态模式主要目的就是将条件判断的不同结构,转化为状态对象的内部状态,既然是状态对象的内部状态,所以一般是作为状态对象的私有变量,然后提供一个能够调用状态对象内部状态的接口方法对象

四. 策略模式

将定义的一组算法封装起来,使其相互之间可以替代。封装的算法具有一定的独立性,不会随着客户端变化而变化

从结构上看,他和状态模式非常的相似,也是在内部封装一个对象,然后通过返回的借口对象实现对内部对象的调用,不同的是,策略模式不需要管理状态,状态之间没有依赖关系,策略之间可以相互替换,在策略对象内部保存的是一些相对独立的一些算法。

五. 职责链模式

解决请求发送者和接受者之间的耦合,通过职责链上多个对象分解请求流程,实现请求在多个对象之间的传递,直到最后一个对象完成请求的处理。

职责链模式的优点是:请求发送者只需要直到链中的第一个节点,从而解耦了发送者和一组接收者之间的强联系。此外,使用了职责链模式之后,链中的节点对象可以灵活地拆分重组,增加或者删除 一个节点,以及改变节点在链中的位置都是轻而易举的。

职责链模式的缺点是:首先不能保证某个请求一定会被链中的某个节点处理,这种情况系下可以在链尾增加一个保底的接受者节点来处理这种即将离开链尾的请求。其次,职责链模式使得程序中多了一些节点对象,可能在某一次的请求传递过程中,大部分的节点并没有起到实质性的作用,从性能的角度考虑,应当避免过长的职责链带来的性能损耗。

场景

假设有这么一种场景:一个售卖手机的电商网站,经过分别交纳 500 元定金和 200 元定金的两轮预订之后(订单在此时已经生成),现在进入了正式购买阶段。

公司针对支付过定金的客户有一定的优惠,正式购买之后,已经支付过 500 元定金的用户会收到 100 元优惠券,200 元定金的用户可以收到 50 元优惠券,没有支付过定金的只能进入普通购买方式,也就是没有优惠券。相关的字段有这么几种:

  • oederType: 订单类型,为 1 代表 500 元定金用户,2 代表 200 元定金用户,3 为普通购买用户;
//职责链模式
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 {
    return 'nextSuccessor';
  }
};

接下来需要把函数包装进职责链节点:

//职责链包装

var Chain = function(fn) {
  this.fn = fn;
  this.nextSuccessor = null;
};

Chain.prototype.setNextSuccessor = function(successor) {
  return (this.nextSuccessor = successor);
};

Chain.prototype.passRequest = function() {
  var ret = this.fn.apply(this, arguments);

  if (ret == 'nextSuccessor') {
    //console.log(this.nextSuccessor.fn.name);
    return (
      this.nextSuccessor &&
      this.nextSuccessor.passRequest.apply(this.nextSuccessor, 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, 100); //输出 500元定金,得到100元优惠券
chainOrder500.passRequest(2, true, 100); //输出200元定金,得到50元优惠券
chainOrder500.passRequest(1, false, 0); //输出 手机库存不足

六. 命令模式

用来对方法调用进行参数化处理和传送,经过这样处理过的方法调用可以在任何需要的时候执行。也就是说该模式旨在将函数的调用、请求和操作封装成一个单一的对象,然后对这个对象进行一些列的处理。他也可以用来消除调用操作的对象和实现操作的对象之间的耦合。这为各种具体的类的更换带来了极大的灵活性。

//1.一个连有炮兵和步兵,司令可以下命令调动军队打仗
var lian = {};
lian.paobing = function(pao_num) {
  console.log(pao_num + '门炮准备战斗');
};
lian.bubing = function(bubing_num) {
  console.log(bubing_num + '人准备战斗');
};
lian.lianzhang = function(mingling) {
  lian[mingling.type](mingling.num);
};

//司令下命令
lian.lianzhang({
  type: 'paobing',
  num: 10
});
lian.lianzhang({
  type: 'bubing',
  num: 100
});

七. 访问者模式

针对于对象结构中的元素,定义在不改变对象的前提下访问结构中元素的方法

在访问者模式中,主要包括下面几个角色

  1. 抽象访问者:抽象类或者接口,声明访问者可以访问哪些元素,具体到程序中就是 visit 方法中的参数定义哪些对象是可以被访问的。
  2. 访问者:实现抽象访问者所声明的方法,它影响到访问者访问到一个类后该干什么,要做什么事情。
  3. 抽象元素类:接口或者抽象类,声明接受哪一类访问者访问,程序上是通过 accept 方法中的参数来定义的。抽象元素一般有两类方法,一部分是本身的业务逻辑,另外就是允许接收哪类访问者来访问。
  4. 元素类:实现抽象元素类所声明的 accept 方法,通常都是visitor.visit(this),基本上已经形成一种定式了。
  5. 结构对象:一个元素的容器,一般包含一个容纳多个不同类、不同接口的容器,如 List、Set、Map 等,在项目中一般很少抽象出这个角色。
// 访问者
function Visitor() {
  this.visit = function(concreteElement) {
    concreteElement.doSomething();
  };
}
// 元素类
function ConceteElement() {
  this.doSomething = function() {
    console.log('这是一个具体元素');
  };
  this.accept = function(visitor) {
    visitor.visit(this);
  };
}
// Client
var ele = new ConceteElement();
var v = new Visitor();
ele.accept(v);