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

定义

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

14.1 现实中的中介者

  • 机场指挥塔
  • 博彩公司

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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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();
}

接下来创建两个玩家对象

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

给玩家相互设置敌人

1
2
player1.enemy=player2
player2.enemy=player1

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

1
player1.die()

14.2.1 为游戏增加队伍

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

1
2
3
4
5
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来给每个玩家设置敌人和队友

1
var players=[];

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

1
2
3
4
5
6
function Player(name,teamColor){
this.partners=[];//队友列表
this.enemies=[];//敌人列表
this.state="live";//玩家状态 初始状态都是存活
this.teamColor=teamColor;//队伍颜色
}

玩家胜利和失败之后的展现依然不变

1
2
3
4
5
6
Player.prototype.win = function(){ // 玩家团队胜利
console.log( 'winner: ' + this.name );
};
Player.prototype.lose = function(){ // 玩家团队失败
console.log( 'loser: ' + this.name );
};

玩家死亡的情况要变得复杂一点,我们需要在每个队友死亡的情况下遍历其他队友的生存情况 如果队友全部死亡 则这一局游戏失败,敌人队伍的所有玩家胜利

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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();
}
}
}

最后定义一个工厂来创建玩家

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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
}

然后用这段代码创建八个玩家

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//红队:
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原型方法中不再负责具体的执行逻辑,而是把操作转交给中介者对象.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
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);//给中介者发送消息,玩家换队
}

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

1
2
3
4
5
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识别消息来自于哪个对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
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开始编写

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<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事件,并且在这两个事件中做出相应的处理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
<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的事件相关代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
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节点

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
<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事件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
<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事件.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
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 引入中介者

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
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)
}

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

小结

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