【青训营】- JavaScript中常见设计模式(下篇)

204 阅读22分钟

行为设计模式

行为型模式封装的是对象的行为变化,用于描述“类或对象之间怎样相互协作共同完成单个对象无法单独完成的任务,以及怎样分配职责”。常见的模式包括策略模式(Strategy)迭代器模式(Iterator)发布-订阅模式(Obsever)命令模式(Command)模板方法模式(Template Method)职责链模式(Chain of Responsibility)中间者模式(Mediator)状态模式(State)

1.策略模式(Strategy)

策略模式将定义一系列的算法封装起来,并且使它们可以相互替换。

以年终奖的计算为例,很多公司的年终奖是根据员工的工资基数和年底绩效情况来发放的。例如,绩效为S的人年终奖有4倍工资,绩效为A的人年终奖有3倍工资,而绩效为B的人年终奖是2倍工资。假设财务部要求我们提供一段代码,来方便他们计算员工的年终奖。

我们可以编写一个名为calculateBonus的函数来计算每个人的奖金数额。很显然,calculateBonus函数需要接收两个参数:员工的工资数额和他的绩效考核等级。代码如下:

    var calculateBonus = function (performanceLevel, salary) {
      if (performanceLevel === 'S') {
        return salary * 4;
      }
      if (performanceLevel === 'A') {
        return salary * 3;
      }
      if (performanceLevel === 'B') {
        return salary * 2;
      }
    };
    calculateBonus('B', 20000); // 输出:40000
    calculateBonus('S', 6000); // 输出:24000

这段代码十分简单,但是存在着显而易见的缺点。calculateBonus函数比较庞大,包含了很多if-else语句,这些语句需要覆盖所有的逻辑分支。且calculateBonus函数缺乏弹性,如果增加了一种新的绩效等级C,或者想把绩效S的奖金系数改为5,那我们必须深入calculateBonus函数的内部实现,这是违反开放-封闭原则的。

我们利用策略模式来改写这段代码。利用策略模式将定义的一系列的算法封装起来

    var strategies = {
      "S": function (salary) {
        return salary * 4;
      },
      "A": function (salary) {
        return salary * 3;
      },
      "B": function (salary) {
        return salary * 2;
      }
    };
    var calculateBonus = function (level, salary) {
      return strategies[level](salary);
    };
    console.log(calculateBonus('S', 20000)); // 输出:80000
    console.log(calculateBonus('A', 10000)); // 输出:30000

将不变的部分和变化的部分隔开是每个设计模式的主题,策略模式也不例外,策略模式的目的就是将算法的使用与算法的实现分离开来。在这个例子里,算法的使用方式是不变的,都是根据某个算法取得计算后的奖金数额。而算法的实现是各异和变化的,每种绩效对应着不同的计算规则。

一个基于策略模式的程序至少由两部分组成。第一个部分是策略组,策略组封装了具体的算法,并负责具体的计算过程。 第二个部分是环境ContextContext接受客户的请求,随后把请求委托给某一个策略。要做到这点,说明Context中要维持对某个策略对象的引用。

2.迭代器模式(Iterator)

迭代器模式是指提供一种方法顺序访问一个聚合对象中的各个元素,而又不需要暴露该对象的内部表示。迭代器模式可以把迭代的过程从业务逻辑中分离出来,在使用迭代器模式之后,即使不关心对象的内部构造,也可以按顺序访问其中的每个元素。

JavaScript内置了迭代器实现,即Array.prototype.forEach。当然,我们也可以自己实现一个迭代器。

    var each = function (ary, callback) {
      for (var i = 0, l = ary.length; i < l; i++) {
        callback.call(ary[i], i, ary[i]); // 把下标和元素当作参数传给callback 函数
      }
    };
    each([1, 2, 3], function (i, n) {
      alert([i, n]);
    });

迭代器可以分为内部迭代器外部迭代器,它们有各自的适用场景。我们刚刚编写的each函数属于内部迭代器,each函数的内部已经定义好了迭代规则,它完全接手整个迭代过程,外部只需要一次初始调用。由于内部迭代器的迭代规则已经被提前规定,上面的each函数就无法同时迭代2个数组了。比如现在有个需求,要判断2个数组里元素的值是否完全相等, 如果不改写each函数本身的代码,我们能够入手的地方似乎只剩下each的回调函数了。

    var compare = function (ary1, ary2) {
      if (ary1.length !== ary2.length) {
        throw new Error('ary1 和ary2 不相等');
      }
      each(ary1, function (i, n) {
        if (n !== ary2[i]) {
          throw new Error('ary1 和ary2 不相等');
        }
      });
      alert('ary1 和ary2 相等');
    };
    compare([1, 2, 3], [1, 2, 4]); // throw new Error ( 'ary1 和ary2 不相等' );

外部迭代器必须显式地请求迭代下一个元素。外部迭代器增加了一些调用的复杂度,但相对也增强了迭代器的灵活性,我们可以手工控制迭代的过程或者顺序。接下来看看如何用外部迭代器改写compare函数。

    var Iterator = function (obj) {
      var current = 0;
      var next = function () {
        current += 1;
      };
      var isDone = function () {
        return current >= obj.length;
      };
      var getCurrItem = function () {
        return obj[current];
      };
      return {
        next: next,
        isDone: isDone,
        getCurrItem: getCurrItem
      }
    };

    var compare = function (iterator1, iterator2) {
      while (!iterator1.isDone() && !iterator2.isDone()) {
        if (iterator1.getCurrItem() !== iterator2.getCurrItem()) {
          throw new Error('iterator1 和iterator2 不相等');
        }
        iterator1.next();
        iterator2.next();
      }
      alert('iterator1 和iterator2 相等');
    }
    var iterator1 = Iterator([1, 2, 3]);
    var iterator2 = Iterator([1, 2, 3]);
    compare(iterator1, iterator2); // 输出:iterator1 和iterator2 相等

3.发布—订阅模式(Obsever)

发布—订阅模式又叫观察者模式,它定义对象间的一种一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都将得到通知。不论是在程序世界里还是现实生活中,发布-订阅模式的应用都非常之广泛。我们先看一个现实中的例子。

小明、小红、小刚都看上了某个楼盘,但售房处告知该房子已经售罄,但后续会有一些尾盘,于是他们交换了联系方式。在不使用发布-订阅模式的时候,三个人每天都会打电话给售楼处,如果有更多的客户,每天的电话甚至更多;而在使用发布-订阅模式时,售楼处会在有房源时,通知三人。

可以发现,在这个例子中使用发布-订阅模式有着显而易见的优点。

  • 购房者不用再天天给售楼处打电话咨询开售时间,在合适的时间点,售楼处作为发布者会通知这些消息订阅者。
  • 购房者和售楼处之间不再强耦合在一起,当有新的购房者出现时,他只需把手机号码留在售楼处,售楼处不关心购房者的任何情况,而售楼处的其他变动也不会影响购买者。

第一点说明发布-订阅模式可以广泛应用于异步编程中,这是一种替代传递回调函数的方案。而第二点说明发布-阅模式可以取代对象之间硬编码的通知机制,一个对象不用再显式地调用另外一个对象的某个接口。

实际上,只要我们曾经在DOM节点上面绑定过事件函数,那我们就曾经使用过发布-订阅模式。除了DOM 事件,我们还会经常实现一些自定义的事件,这种依靠自定义事件完成的发布-订阅模式可以用于任何JavaScript代码中。

    var salesOffices = {}; // 定义售楼处
    salesOffices.clientList = []; // 缓存列表,存放订阅者的回调函数
    salesOffices.listen = function (fn) { // 增加订阅者
      this.clientList.push(fn); // 订阅的消息添加进缓存列表
    };
    salesOffices.trigger = function () { // 发布消息
      for (var i = 0, fn; fn = this.clientList[i++];) {
        fn.apply(this, arguments); // (2) // arguments 是发布消息时带上的参数
      }
    };

    salesOffices.listen(function (price, squareMeter) { // 小明订阅消息
      console.log('价格= ' + price);
      console.log('squareMeter= ' + squareMeter);
    });
    salesOffices.listen(function (price, squareMeter) { // 小红订阅消息
      console.log('价格= ' + price);
      console.log('squareMeter= ' + squareMeter);
    });
    salesOffices.trigger(2000000, 88); // 输出:200 万,88 平方米
    salesOffices.trigger(3000000, 110); // 输出:300 万,110 平方米

4.命令模式(Command)

命令模式(Command)中的命令指的是一个执行某些特定事情的指令。最常见的应用场景是:有时候需要向某些对象发送请求,但是并不知道请求的接收者是谁,也不知道被请求的操作是什么。此时希望用一种松耦合的方式来设计程序,使得请求发送者和请求接收者能够消除彼此之间的耦合关系

设想我们正在编写一个用户界面程序,该用户界面上至少有数十个Button按钮。因为项目比较复杂,所以我们决定让某个程序员负责绘制这些按钮,而另外一些程序员则负责编写点击按钮后的具体行为,这些行为都将被封装在对象里。对于绘制按钮的程序员来说,他完全不知道某个按钮未来将用来做什么,可能用来刷新菜单界面,也可能用来增加一些子菜单,他只知道点击这个按钮会发生某些事情。

我们很快可以找到在这里运用命令模式的理由:点击了按钮之后,必须向某些负责具体行为的对象发送请求,这些对象就是请求的接收者。但是目前并不知道接收者是什么对象,也不知道接收者究竟会做什么。此时我们需要借助命令对象的帮助,以便解开按钮和负责具体行为对象之间的耦合。

  <button id="button1">点击按钮1</button>
  <button id="button2">点击按钮2</button>
  <button id="button3">点击按钮3</button>
    var button1 = document.getElementById('button1');
    var button2 = document.getElementById('button2');
    var button3 = document.getElementById('button3');
    var MenuBar = {
      refresh: function () {
        console.log('刷新菜单目录');
      }
    };
    var SubMenu = {
      add: function () {
        console.log('增加子菜单');
      },
      del: function () {
        console.log('删除子菜单');
      }
    };

    var RefreshMenuBarCommand = function (receiver) {
      return {
        execute: function () {
          receiver.refresh()
        }
      }
    };
    var AddSubMenuCommand = function (receiver) {
      return {
        execute: function () {
          receiver.add()
        }
      }
    };
    var DelSubMenuCommand = function (receiver) {
      return {
        execute: function () {
          receiver.del()
        }
      }
    };
    var setCommand = function (button, command) {
      button.onclick = function () {
        command.execute();
      }
    };
    var refreshMenuBarCommand = RefreshMenuBarCommand(MenuBar);
    var addSubMenuCommand = AddSubMenuCommand(SubMenu);
    var delSubMenuCommand = DelSubMenuCommand(SubMenu);
    setCommand(button1, refreshMenuBarCommand);
    setCommand(button2, addSubMenuCommand);
    setCommand(button3, delSubMenuCommand);

点击按钮会执行某个命令,执行命令的动作被约定为调用command对象的execute()方法。虽然还不知道这些命令究竟代表什么操作,但负责绘制按钮的程序员不关心这些事情,他只需要预留好安装命令的接口,command对象自然知道如何和正确的对象沟通

5.模板方法模式(Template Method)

模板方法模式由两部分结构组成,第一部分是抽象父类,第二部分是具体的实现子类。通常在抽象父类中封装了子类的算法框架,包括实现一些公共方法以及封装子类中所有方法的执行顺序。子类通过继承这个抽象类,也继承了整个算法结构,并且可以选择重写父类的方法。

假如我们有一些平行的子类,各个子类之间有一些相同的行为,也有一些不同的行为。如果相同和不同的行为都混合在各个子类的实现中,说明这些相同的行为会在各个子类中重复出现。但实际上,相同的行为可以被搬移到另外一个单一的地方,模板方法模式就是为解决这个问题而生的。在模板方法模式中,子类实现中的相同部分被上移到父类中,而将不同的部分留待子类来实现

咖啡与茶是一个经典的例子,经常用来讲解模板方法模式,这个例子的原型来自《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();

对比泡咖啡和泡茶的过程

Snipaste_2021-09-17_14-25-54.png

我们找到泡咖啡和泡茶主要有以下不同点。

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

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

  1. 把水煮沸
  2. 用沸水冲泡饮料
  3. 把饮料倒进杯子
  4. 加调料

现在可以创建一个抽象父类来表示泡一杯饮料的整个过程。

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

接下来我们要创建咖啡类,并让它继承饮料类:

    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类已经完成了,当调用coffee对象init方法时,由于coffee对象Coffee构造器的原型prototype上都没有对应的init方法,所以该请求会顺着原型链,被委托给Coffee的“父类”Beverage原型上的init方法。

Beverage.prototype.init方法中已经规定好了泡饮料的顺序,所以我们能成功地泡出一杯咖啡。Beverage.prototype.init就是模板方法。

6.职责链模式(Chain of Responsibility)

职责链模式的名字非常形象,一系列可能会处理请求的对象被连接成一条链,请求在这些对象之间依次传递,直到遇到一个可以处理它的对象,我们把这些对象称为链中的节点.

图片2.png

职责链模式的例子在现实中并不难找到。当你在高峰时挤上公交,常常因为公交车上人太多而无法投币,所以只好把两块钱硬币往前面递。除非你运气够好,否则,你的硬币通常要在N个人手上传递,才能最终到达投币箱里。

职责链模式的最大的优点是请求发送者只需要知道链中的第一个节点,从而弱化了发送者和一组接收者之间的强联系。如果不使用职责链模式,那么在公交车上,我就得先走到投币箱前,才能投币。

假设我们负责一个售卖手机的电商网站,经过分别交纳500元定金和200元定金的两轮预定后,现在已经到了正式购买的阶段。公司针对支付过定金的用户有一定的优惠政策。在正式购买后,已经支付过500元定金的用户会收到100元的商城优惠券,200元定金的用户可以收到50元的优惠券,而之前没有支付定金的用户只能进入普通购买模式,也就是没有优惠券,且在库存有限的情况下不一定保证能买到。

我们把这个流程写成代码:

    var order = function (orderType, pay, stock) {
      if (orderType === 1) { // 500 元定金购买模式
        if (pay === true) { // 已支付定金
          console.log('500 元定金预购, 得到100 优惠券');
        } else { // 未支付定金,降级到普通购买模式
          if (stock > 0) { // 用于普通购买的手机还有库存
            console.log('普通购买, 无优惠券');
          } else {
            console.log('手机库存不足');
          }
        }
      }
      else if (orderType === 2) { // 200 元定金购买模式
        if (pay === true) {
          console.log('200 元定金预购, 得到50 优惠券');
        } else {
          if (stock > 0) {
            console.log('普通购买, 无优惠券');
          } else {
            console.log('手机库存不足');
          }
        }
      }
      else if (orderType === 3) {
        if (stock > 0) {
          console.log('普通购买, 无优惠券');
        } else {
          console.log('手机库存不足');
        }
      }
    };

虽然我们得到了意料中的运行结果,但order函数不仅难以阅读,而且需要经常进行修改。现在我们采用职责链模式重构这段代码。

    var order500yuan = function (orderType, pay, stock) {
      if (orderType === 1 && pay === true) {
        console.log('500 元定金预购,得到100 优惠券');
      } else {
        return 'nextSuccessor'; // 我不知道下一个节点是谁,反正把请求往后面传递
      }
    };
    var order200yuan = 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('手机库存不足');
      }
    };

    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 = order500yuan.after(order200yuan).after(orderNormal);
    order(1, true, 500); // 输出:500 元定金预购,得到100 优惠券
    order(2, true, 500); // 输出:200 元定金预购,得到50 优惠券
    order(1, false, 500); // 输出:普通购买,无优惠券

职责链模式可以很好地帮助我们组织代码,但这种模式也并非没有弊端,首先我们不能保证某个请求一定会被链中的节点处理,在这种情况下,我们可以在链尾增加一个保底的接受者节点来处理这种即将离开链尾的请求。另外,职责链模式使得程序中多了一些节点对象,可能在某一次的请求传递过程中,大部分节点并没有起到实质性的作用,它们的作用仅仅是让请求传递下去,从性能方面考虑,我们要避免过长的职责链带来的性能损耗

7.中间者模式(Mediator)

在程序里,也许一个对象会和多个对象打交道,保持对其引用。当程序的规模增大,对象会越来越多,它们之间的关系也越来越复杂,难免会形成网状的交叉引用。当我们改变或删除其中一个对象的时候,很可能需要通知所有引用到它的对象。这样一来,就像在心脏旁边拆掉一根毛细血管一般,即使一点很小的修改也必须小心翼翼。

图片1.png

中介者模式的作用就是解除对象与对象之间的紧耦合关系。增加一个中介者对象后,所有的相关对象都通过中介者对象来通信,而不是互相引用,所以当一个对象发生改变时,只需要通知中介者对象即可。

图片2.png

在现实生活中也有很多中介者的例子,如飞机场指挥调度飞机、博彩公司计算赔率和输赢。

现在有一个支持多人对战的游戏。先定义一个玩家构造函数,它有3个简单的原型方法Player.prototype.winPlayer.prototype.lose以及表示玩家死亡的Player.prototype.die

    var players = [];
    function Player(name, teamColor) {
      this.partners = []; // 队友列表
      this.enemies = []; // 敌人列表
      this.state = 'live'; // 玩家状态
      this.name = name; // 角色名字
      this.teamColor = teamColor; // 队伍颜色
    };
    Player.prototype.win = function () { // 玩家团队胜利
      console.log('winner: ' + this.name);
    };
    Player.prototype.lose = function () { // 玩家团队失败
      console.log('loser: ' + this.name);
    };
    Player.prototype.die = function () { // 玩家死亡
      var all_dead = true;
      this.state = 'dead'; // 设置玩家状态为死亡
      for (var i = 0, partner; partner = this.partners[i++];) { // 遍历队友列表
        if (partner.state !== 'dead') { // 如果还有一个队友没有死亡,则游戏还未失败
          all_dead = false;
          break;
        }
      }
      if (all_dead === true) { // 如果队友全部死亡
        this.lose(); // 通知自己游戏失败
        for (var i = 0, partner; partner = this.partners[i++];) { // 通知所有队友玩家游戏失败
          partner.lose();
        }
        for (var i = 0, enemy; enemy = this.enemies[i++];) { // 通知所有敌人游戏胜利
          enemy.win();
        }
      }
    };
    //定义一个工厂来创建玩家
    var playerFactory = function (name, teamColor) {
      var newPlayer = new Player(name, teamColor); // 创建新玩家
      for (var i = 0, player; player = players[i++];) { // 通知所有的玩家,有新角色加入
        if (player.teamColor === newPlayer.teamColor) { // 如果是同一队的玩家
          player.partners.push(newPlayer); // 相互添加到队友列表
          newPlayer.partners.push(player);
        } else {
          player.enemies.push(newPlayer); // 相互添加到敌人列表
          newPlayer.enemies.push(player);
        }
      }
      players.push(newPlayer);
      return newPlayer;
    };

    //红队:
    var player1 = playerFactory('皮蛋', 'red'),
      player2 = playerFactory('小乖', 'red'),
      player3 = playerFactory('宝宝', 'red'),
      player4 = playerFactory('小强', 'red');
    //蓝队:
    var player5 = playerFactory('黑妞', 'blue'),
      player6 = playerFactory('葱头', 'blue'),
      player7 = playerFactory('胖墩', 'blue'),
      player8 = playerFactory('海盗', 'blue');
    // 让红队玩家全部死亡
    player1.die();
    player2.die();
    player4.die();
    player3.die();

结果如图所示

Snipaste_2021-09-18_16-17-44.png

现在我们已经可以随意地为游戏增加玩家或者队伍,但问题是,每个玩家和其他玩家都是紧紧耦合在一起的。当每个对象的状态发生改变,比如角色移动、吃到道具或者死亡时,都必须要显式地遍历通知其他对象。而如果在一个大型网络游戏中,情况将更复杂,还存在掉线,转换阵营的问题。

现在我们开始用中介者模式来改造游戏。Player不再负责具体的执行逻辑,而是把操作转交给中介者对象playerDirector

   function Player(name, teamColor) {
      this.name = name; // 角色名字
      this.teamColor = teamColor; // 队伍颜色
      this.state = 'alive'; // 玩家生存状态
    };
    Player.prototype.win = function () {
      console.log(this.name + ' won ');
    };
    Player.prototype.lose = function () {
      console.log(this.name + ' lost');
    };
    /*******************玩家死亡*****************/
    Player.prototype.die = function () {
      this.state = 'dead';
      playerDirector.reciveMessage('playerDead', this); // 给中介者发送消息,玩家死亡
    };
    /*******************移除玩家*****************/
    Player.prototype.remove = function () {
      playerDirector.reciveMessage('removePlayer', this); // 给中介者发送消息,移除一个玩家
    };
    /*******************玩家换队*****************/
    Player.prototype.changeTeam = function (color) {
      playerDirector.reciveMessage('changeTeam', this, color); // 给中介者发送消息,玩家换队
    };

    var playerFactory = function (name, teamColor) {
      var newPlayer = new Player(name, teamColor); // 创造一个新的玩家对象
      playerDirector.reciveMessage('addPlayer', newPlayer); // 给中介者发送消息,新增玩家
      return newPlayer;
    };

    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 reciveMessage = function () {
        var message = Array.prototype.shift.call(arguments); // arguments 的第一个参数为消息名称
        operations[message].apply(this, arguments);
      };
      return {
        reciveMessage: reciveMessage
      }
    })();

    // 红队:
    var player1 = playerFactory('皮蛋', 'red'),
      player2 = playerFactory('小乖', 'red'),
      player3 = playerFactory('宝宝', 'red'),
      player4 = playerFactory('小强', 'red');
    // 蓝队:
    var player5 = playerFactory('黑妞', 'blue'),
      player6 = playerFactory('葱头', 'blue'),
      player7 = playerFactory('胖墩', 'blue'),
      player8 = playerFactory('海盗', 'blue');
    player1.die();
    player2.die();
    player3.die();
    player4.die();

可以看到,除了中介者本身,没有一个玩家知道其他任何玩家的存在,玩家与玩家之间的耦合关系已经完全解除,某个玩家的任何操作都不需要通知其他玩家,而只需要给中介者发送一个消息,中介者处理完消息之后会把处理结果反馈给其他的玩家对象。

不过,中介者模式也存在一些缺点。其中,最大的缺点是系统中会新增一个中介者对象,因为对象之间交互的复杂性,转移成了中介者对象的复杂性,使得中介者对象经常是巨大的。中介者对象自身往往就是一个难以维护的对象。

8.状态模式(State)

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

我们来想象这样一个场景:有一个电灯,电灯上面只有一个开关。当电灯开着的时候,此时按下开关,电灯会切换到关闭状态;再按一次开关,电灯又将被打开。同一个开关按钮,在不同的状态下,表现出来的行为是不一样的。

首先给出不用状态模式的电灯程序实现:

    var Light = function () {
      this.state = 'off'; // 给电灯设置初始状态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.buttonWasPressed();
      }
    };

    Light.prototype.buttonWasPressed = function () {
      if (this.state === 'off') {
        console.log('开灯');
        this.state = 'on';
      } else if (this.state === 'on') {
        console.log('关灯');
        this.state = 'off';
      }
    };
    var light = new Light();
    light.init();

令人遗憾的是,这个世界上的电灯并非只有一种。许多酒店里有另外一种电灯,这种电灯也只有一个开关,但它的表现是:第一次按下打开弱光,第二次按下打开强光,第三次才是关闭电灯。而这些必须改造上面的代码来完成这种新型电灯的制造。

但将状态改变的逻辑全部写在buttonWasPressed中存在以下问题:

  1. 违反开放-封闭原则的,每次新增或者修改light的状态,都需要改动buttonWasPressed方法中的代码。
  2. 所有跟状态有关的行为,都被封装在buttonWasPressed方法里,我们将无法预计这个方法膨胀到什么地步。
  3. 状态的切换非常不明显,仅仅表现为对state变量赋值,我们也没有办法一目了然地明白电灯一共有多少种状态。
  4. 状态之间的切换关系,不过是往buttonWasPressed方法里堆砌ifelse语句,这使buttonWasPressed更加难以阅读和维护

现在我们学习使用状态模式改进电灯的程序。状态模式的关键是把事物的每种状态都封装成单独的类,跟此种状态有关的行为都被封装在这个类的内部,所以button被按下的的时候,只需要在上下文中,把这个请求委托给当前的状态对象即可,该状态对象会负责渲染它自身的行为。

    // OffLightState:
    var OffLightState = function (light) {
      this.light = light;
    };
    OffLightState.prototype.buttonWasPressed = function () {
      console.log('弱光'); // offLightState 对应的行为
      this.light.setState(this.light.weakLightState); // 切换状态到weakLightState
    };
    // WeakLightState:
    var WeakLightState = function (light) {
      this.light = light;
    };
    WeakLightState.prototype.buttonWasPressed = function () {
      console.log('强光'); // weakLightState 对应的行为
      this.light.setState(this.light.strongLightState); // 切换状态到strongLightState
    };
    // StrongLightState:
    var StrongLightState = function (light) {
      this.light = light;
    };
    StrongLightState.prototype.buttonWasPressed = function () {
      console.log('关灯'); // strongLightState 对应的行为
      this.light.setState(this.light.offLightState); // 切换状态到offLightState
    };

    var Light = function () {
      this.offLightState = new OffLightState(this);
      this.weakLightState = new WeakLightState(this);
      this.strongLightState = new StrongLightState(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();
      }
    };
    Light.prototype.setState = function (newState) {
      this.currState = newState;
    };
    var light = new Light();
    light.init();

执行结果跟之前的代码一致,但是使用状态模式的好处很明显,它可以使每一种状态和它对应的行为之间的关系局部化,这些行为被分散和封装在各自对应的状态类之中,便于阅读和管理代码。

另外,状态之间的切换都被分布在状态类内部,这使得我们无需编写过多的ifelse条件分支语言来控制状态之间的转换。

参考资料

《JavaScript设计模式与开发实践》
《Head First设计模式》