JavaScript设计模式笔记-中介者模式

定义

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

14.1 现实中的中介者

  • 机场指挥塔
  • 博彩公司

14.2 中介者模式的例子——泡泡堂游戏

初期泡泡堂只支持双人对战.我们来定义一个构造函数 有三个方法Play.prototype.win,Play.prototype.lose,Play.prototype.die因为是两个人 所以当一个玩家死亡的时候游戏便结束

var Player=function(){ 
   this.name=name 
   this.enemy=null;//敌人 
} 
Player.prototype.win=function(){ 
   console.log(this.name+"won") 
} 
Player.prototype.lose=function(){ 
   console.log(this.name+"lost") 
} 
Player.prototype.die=function(){ 
   this.lose(); 
   this.enemy.win(); 
} 

接下来创建两个玩家对象

var player1=new Player("皮蛋") 
var player2=new Player("小乖") 

给玩家相互设置敌人

player1.enemy=player2 
player2.enemy=player1 

当player1被炸死的时候,只需要调用这一句代码便完成了一局游戏

player1.die() 

14.2.1 为游戏增加队伍

现在我们改进下游戏 分成红蓝双方,每队都有4个人.用下面的方式设置无疑是很低效的

player1.partners=["player1","player2","player3","player4"] 
player1.enemies=["player5","player6","player7","player8"] 

player5.partners=["player5","player6","player7","player8"] 
player5.enemies=["player1","player2","player3","player4"] 

于是我们来改写一下 用一个数组players来保存所有玩家,在创建玩家之后,循环players来给每个玩家设置敌人和队友

var players=[]; 

再改写构造函数Player,使每个玩家对象都增加一些属性,分别是队友列表,敌人列表,玩家当前状态,角色名字以及玩家所在的队伍颜色

function Player(name,teamColor){ 
   this.partners=[];//队友列表 
   this.enemies=[];//敌人列表 
   this.state="live";//玩家状态 初始状态都是存活 
   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,partners;partners=this.partners[i++]){//遍历队友列表 
       if(partners.state!=="dead"){//如果队友中还有一个玩家没死 
           all_dead=false;//关闭全部死亡这个设定 
           break;//跳出循环 
       } 
   } 
   if(all_dead===true){//如果队友全部死亡 
       this.lose();//通知自己游戏失败 
       for(var i=0,partners;partners=this.partners[i++]){//通知队友游戏失败 
           partners.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===new Player.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(); 

执行后不出所料 蓝队会胜利 红队是失败的

14.2.2 玩家增多带来的困扰

上述的代码是紧紧耦合在一起的.在此段代码中,每个玩家都有两个属性,this.partners和this.enemies,用来保存其他玩家对象的引用.每当玩家状态发送改变都需要显式的通知其他玩家.如果需求再复杂一点 比如解除队伍和替换队伍 上述代码可以很快GG

14.2.3 用中介者模式来改造泡泡堂游戏

首先仍然是定义Player构造函数和player对象的原型方法.在player原型方法中不再负责具体的执行逻辑,而是把操作转交给中介者对象.

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+"lose") 
} 
   //玩家死亡 
Player.prototype.die=function(){ 
   this.state="dead"; 
   playerDirector.ReceiveMessage("playerDead",this);//给中介者发送消息,玩家死亡 
} 
   //移除玩家 
Player.prototype.remove=function(){ 
   playerDirector.ReceiveMessage("removePlayer",this);//给中介者发送消息,移除一个玩家 
} 
   //玩家换队 
Player.prototype.changeTeam=function(color){ 
  playerDirector.ReceiveMessage("changeTeam",this,color);//给中介者发送消息,玩家换队 
} 

再继续改写之前创建玩家对象的工厂函数,可以看到,因为工厂函数里不需要再给创建的玩家设置队友和敌人,这个工厂函数几乎失去了工厂的意义

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

最后, 我们需要实现playerDirector对象 一般有以下两种方式

  • 利用发布-订阅模式.将playerDirector实现为订阅者,各player作为发布者.一旦player的状态发生改变,便将消息推送给playerDirector.playerDirector处理消息后将反馈给其他player
  • 在playerDirector中开放一些接收消息的接口,各player可以直接调用该接口来给playerDirector发送消息,player只需要传递一个参数给playerDirector,这个参数的目的是使playerDirector可以识别发送者.同样.playerDirector处理消息后将反馈给其他player

这里采用第二种方式.playerDirector开放一个对外暴露的接口ReceiveMessage.负责接收player对象发送的消息,而player对象发送消息的时候,总是把自身this作为参数发送给playerDirector,以便playerDirector识别消息来自于哪个对象

var playerDirector=(function(){ 
   var players={};//保存所有玩家 
   var 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;//玩家队伍颜色 
       var 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;//玩家队伍 
       var 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();//向敌方玩家通知胜利 
                   } 
               } 
           } 
       } 
   } 
   var ReceiveMessage=function(){ 
       var message=Array.prototype.shift.call(arguments);//argument的第一个参数为消息名称 
       operations[message].apply(this,argument);//找到相应的处理函数进行处理 
   } 
   return{ 
       ReceiveMessage:ReceiveMessage 
   } 
})() 

可以看到除了中介者本身,没有一个玩家需要知道其他任何玩家的存在,玩家与玩家之间的耦合关系以及完全解除.我们还可以给中介者扩展更多的功能,以适应游戏需求的不断变化.运行与演示请购买本书,这里不做笔记

14.3 中介者模式的例子——购买商品

页面有两个区域 选择购买内容 输入购买数量 购买按钮相应的样式详细介绍请参考原书上内容

我们需要定义5个节点

  • 下拉选择框 colorSelect
  • 文本输入框 numberInput
  • 展示颜色信息 colorInfo
  • 展示购买数量信息 numberInfo
  • 决定下一步操作的按钮 nextBtn

14.3.1 开始编写代码

从HTML开始编写

<body> 
 选择颜色: 
 <select id="colorSelect"> 
   <option value="">请选择</option> 
   <option value="red">红色</option> 
   <option value="blue">蓝色</option> 
 </select> 
 输入购买数量: 
 <input type="text" id="numberInput" /> 
 您选择了颜色: 
 <div id="colorInfo"> 
 </div><br> 
 您输入了数量: 
 <div id="numberInfo"> 
 </div><br> 
 <button id="nextBtn" disabled="true">请选择手机颜色和购买数量</button> 
</body> 

接下来分别监听colorSelect的onchange事件和numberInput的oninput事件,并且在这两个事件中做出相应的处理

<script type="text/javascript"> 

   var colorSelect=document.getElementById("colorSelect"), 
       numberInput=document.getElementById("numberInput"), 
       colorInfo=document.getElementById("colorInfo"), 
       numberInfo=document.getElementById("numberInfo"), 
       nextBtn=document.getElementById("nextBtn"); 

   var goods={//手机库存 
       "red":3, 
       "blue":6 
   } 
   colorSelect.onchange=function(){ 
       var color=this.value,//颜色 
           number=numberInput.value,//数量 
           stock=goods[color];//该颜色的当前库存 
       colorInfo.innerHTML=color; 
       if(!color){ 
           nextBtn.disable=true; 
           nextBtn.innerHTML="请选择手机颜色"; 
           return 
       } 
       if(((number-0)|0)!==number-0){//用户输入的购买数量是否为正整数 
           nextBtn.disable=true; 
           nextBtn.innerHTML="请输入正确的购买数量"; 
           return 
       } 
       if(number>stock){//当前选择数量超过库存 
           nextBtn.disable=true; 
           nextBtn.innerHTML="库存不足"; 
           return 
       } 
       nextBtn.disable=false; 
       nextBtn.innerHTML="放入购物车"; 
   } 
</script> 

14.3.2 对象之间的联系

当触发了colorSelect的onchange后,首先要让colorInfo中显示当前选中的颜色,然后获取用户当前输入的购买数量,对用户的输入值进行一些合法判断,再根据库存数量来判断nextBtn的显示状态.然后来编写numberInput的事件相关代码:

numberInput.oninput=function(){ 
   var color=this.value,//颜色 
       number=numberInput.value,//数量 
       stock=goods[color];//该颜色的当前库存 
    
   numberInfo.innerHTML=number; 

   if(!color){ 
           nextBtn.disable=true; 
           nextBtn.innerHTML="请选择手机颜色"; 
           return 
   } 
   if(((number-0)|0)!==number-0){//用户输入的购买数量是否为正整数 
       nextBtn.disable=true; 
       nextBtn.innerHTML="请输入正确的购买数量"; 
       return 
   } 
   if(number>stock){//当前选择数量超过库存 
       nextBtn.disable=true; 
       nextBtn.innerHTML="库存不足"; 
       return 
   } 
   nextBtn.disable=false; 
   nextBtn.innerHTML="放入购物车"; 
} 

14.3.3 可能遇到的困难

虽然目前顺利完成代码编写.但是如果有新需求加入那么又要重写一边代码.比如我们接下来要增加一个新的下拉栏,代表选择手机内存.现在我们需要计算 颜色 内存 和购买数量来判断nextBtn是显示库存不足还是放入购物车现在我们要增加两个HTML节点

<body> 
 选择颜色: 
 <select id="colorSelect"> 
   <option value="">请选择</option> 
   <option value="red">红色</option> 
   <option value="blue">蓝色</option> 
 </select> 
 选择颜色: 
 <select id="memorySelect"> 
   <option value="">请选择</option> 
   <option value="32G">32G</option> 
   <option value="16G">16G</option> 
 </select> 
 输入购买数量: 
 <input type="text" id="numberInput" /> 
 您选择了颜色: 
 <div id="colorInfo"> 
 </div><br> 
 您选择了内存: 
 <div id="memoryInfo"> 
 </div><br> 
 您输入了数量: 
 <div id="numberInfo"> 
 </div><br> 
 <button id="nextBtn" disabled="true">请选择手机颜色和购买数量</button> 
</body> 
<script type="text/javascript"> 

   var colorSelect=document.getElementById("colorSelect"), 
       memorySelect=document.getElementById("memorySelect"), 
       numberInput=document.getElementById("numberInput"), 
       colorInfo=document.getElementById("colorInfo"), 
       memoryInfo=document.getElementById("memoryInfo"), 
       numberInfo=document.getElementById("numberInfo"), 
       nextBtn=document.getElementById("nextBtn"); 

</script> 

接下来修改表示存库的JSON对象以及修改colorSelect的onchange事件

<script type="text/javascript"> 
   var goods={//手机库存 
       "red|32G":3,//红色32G,库存数量为3 
       "red|16G":0, 
       "blue|16G":1, 
       "blue|16G":6, 
   } 
   colorSelect.onchange=function(){ 
       var color=this.value, 
           memory=memorySelect.value, 
           stock=goods[color+"|"+memory]; 
       number=numberInput.value;//数量 
       colorInfo.innerHTML=color; 
       if(!color){ 
               nextBtn.disable=true; 
               nextBtn.innerHTML="请选择手机颜色"; 
               return 
       } 
       if(!memory){ 
            nextBtn.disable=true; 
            nextBtn.innerHTML="请选择手机内存"; 
            return 
       } 
       if(((number-0)|0)!==number-0){//用户输入的购买数量是否为正整数 
           nextBtn.disable=true; 
           nextBtn.innerHTML="请输入正确的购买数量"; 
           return 
       } 
       if(number>stock){//当前选择数量超过库存 
           nextBtn.disable=true; 
           nextBtn.innerHTML="库存不足"; 
           return 
       } 
       nextBtn.disable=false; 
       nextBtn.innerHTML="放入购物车"; 
   } 
</script> 

同样我们需要改变numberInput的事件相关代码…..我的天…还要新增加一个memorySelect的onchange事件.

memory.onchange=function(){ 
   var color=this.value, 
       memory=memorySelect.value, 
       stock=goods[color+"|"+memory]; 
       memoryInfo.innerHTML=memory; 
   if(!color){ 
       nextBtn.disable=true; 
       nextBtn.innerHTML="请选择手机颜色"; 
       return 
   } 
   if(!memory){ 
        nextBtn.disable=true; 
        nextBtn.innerHTML="请选择手机内存"; 
        return 
   } 
   if(((number-0)|0)!==number-0){//用户输入的购买数量是否为正整数 
       nextBtn.disable=true; 
       nextBtn.innerHTML="请输入正确的购买数量"; 
       return 
   } 
   if(number>stock){//当前选择数量超过库存 
       nextBtn.disable=true; 
       nextBtn.innerHTML="库存不足"; 
       return 
   } 
   nextBtn.disable=false; 
   nextBtn.innerHTML="放入购物车"; 

} 

仅仅是增加一个内存选择条件,就需要改动所有的事件监听函数…每个节点对象都是耦合在一起的

14.3.4 引入中介者

引入中介者对象,所有的节点对象都只跟中介者通信

var goods={//手机库存 
   "red|32G":3, 
   "red|16G":0, 
   "blue|32G":1, 
   "blue|16G":6, 
} 
var mediator=(function(){ 
   var colorSelect=document.getElementById("colorSelect"), 
       memorySelect=document.getElementById("memorySelect"), 
       numberInput=document.getElementById("numberInput"), 
       colorInfo=document.getElementById("colorInfo"), 
       memoryInfo=document.getElementById("memoryInfo"), 
       numberInfo=document.getElementById("numberInfo"), 
       nextBtn=document.getElementById("nextBtn"); 
   return{ 
       changed:function(obj){ 
           var color=colorSelect.value,//颜色 
               memory=memorySelect.value,//内存 
               number=numberInput.value,//数量 
               stock=goods[color+"|"+memory];//颜色和内存对应的手机库存数量 

           if(obj===colorSelect){//如果改变的是选择颜色下拉框 
               colorInfo.innerHTML=color 
           }else if(obj==memorySelect){ 
               memoryInfo.innerHTML=memory 
           }else if(obj===numberInput){ 
               numberInfo.innerHTML=number 
           } 
            
           if(!color){ 
               nextBtn.disable=true; 
               nextBtn.innerHTML="请选择手机颜色"; 
               return 
           } 
           if(!memory){ 
                nextBtn.disable=true; 
                nextBtn.innerHTML="请选择手机内存"; 
                return 
           } 
           if(((number-0)|0)!==number-0){//用户输入的购买数量是否为正整数 
               nextBtn.disable=true; 
               nextBtn.innerHTML="请输入正确的购买数量"; 
               return 
           } 
           if(number>stock){//当前选择数量超过库存 
               nextBtn.disable=true; 
               nextBtn.innerHTML="库存不足"; 
               return 
           } 
           nextBtn.disable=false; 
           nextBtn.innerHTML="放入购物车"; 

       } 
   } 
})() 
//事件函数 
colorSelect.onchange=function(){ 
   mediator.changed(this); 
}; 
memorySelect.onchange=function(){ 
   mediator.changed(this); 
} 
numberInput.oninput=function(){ 
   mediator.changed(this) 
} 

可以想象,某天我们又要新增一些跟需求相关的节点.我们只需要改动中介者函数即可

小结

中介者模式是迎合迪米特法则的中实现,迪米特法则也叫最小知识原则.是指一个对象应该尽可能的少了解另外的对象.中介者模式可以非常方便的对模块进行解耦,但是中介者本身就是一个很难维护的对象.毕竟我们写程序是为了快速完成项目需求,而不是堆砌模式和过度设计.关键就在于如何衡量对象之间的耦合程度.