在这个世界上,一个人大概能记住10个朋友的电话、30家餐馆的位置。
在程序员的世界里,一个前端处于研发的名副其实的中心位置(虽然很多人不愿意承认),开发过程会同时接受其他10个对象包括PM,美工,测试,后端乃至前端同事等踢过来的皮球,所以他会保持10个对象的引用。除了前端,其他对象之间也会存在相互踢球的行为。当程序的规模增大,对象会越来越多,他们之间的关系也越来越复杂,难免会形成网状的交叉引用。当我们修改与其中一个对象关系的时候,很可能需要通知所有引用到它的对象。
而中介者模式的作用就是解除对象与对象之间的紧耦合关系(你或许需要一个秘书)。增加一个中介者对象后,所有的相关对象都通过中介者对象来通信,而不是互相引用,所以当一个对象发生改变时,只需要通知中介者对象即可。中介者使各对象之间耦合松散,而且可以独立地改变它们之间的交互。于是当各种人来找你改bug,可以霸气地告诉他们,"和我秘(lao)书(da)谈吧"。
好吧,我通常的做法是:"写个文档给我吧。"
泡泡堂这个游戏,一下把笔者拉回了十五六年前的回忆。或许,现在应该用吃鸡来举例更贴切吧。
现在你有了技术,要做一个双人对局的游戏泡泡堂,每个游戏玩家有三个简单的原型方法:win/lose/die。业务逻辑是:当玩家A死了,玩家B就赢了。
class Player{
constructor(name){
this.name=name;
// 敌人
this.enemy=null;
}
win(){
console.log(this.name,'win');
}
lose(){
console.log(this.name,'lose');
}
// 通知对手赢了
die(){
this.lose();
this.enemy.win();
}
}
操作的方式也非常简单:
// 创建角色
const pidan=new Player('皮蛋');
const xiaoguai=new Player('小乖');
// 相互设置为敌人
pidan.enemy=xiaoguai;
xiaoguai.enemy=pidan;
// 当皮蛋在游戏中被炸死:
pidan.die();//输出:皮蛋lost、小乖won
两人世界,关系是如此单纯。但这个游戏想必不怎么好玩。
现在我想为游戏支持多人对局。用上面的代码来增加队友和对手显然十分低效。
首先定义个全局的players数组,存放参与的人:
let players=[];
然后增加若干属性:
class Player{
constructor(name,teamColor){
this.partners=[];// 队友
this.enemies=[];// 对手
this.state='live' //生存状态
this.teamColor=teamColor;// 队伍颜色
this.name=name;
}
win(){
console.log(this.name,'win');
}
lose(){
console.log(this.name,'lose');
}
// ...
}
判输的逻辑也变了。每当游戏有人死亡,都会判断场面上此人所有队友的生存状况,如果都死了,就通知这队的所有人,你输了。
die(){
this.state='dead';
var all_die=true;
// 判断队友都死光没
this.partners.forEach((partner)=>{
if(partner.state!=='dead'){
all_die=false;
break;
}
});
if(all_die){
this.lose();
this.partners.forEach((partner)=>{
partner.lose();
});
// 通知敌人赢了
this.enemies.forEach((enemy)=>{
enemy.win();
})
}
}
然后定义一个工厂模式来生成玩家:
const playerFatory=(name,teamColor)=>{
var newPalayer= new Player(name,teamColor);
players.forEach((player)=>{
if(player.teamColor==newPalayer.teamColor){
// 和同色玩家建立队友关系
player.partners.push(newPalayer);
newPalayer.partners.push(player);
}else{
// 否则就是敌人
player.enemies.push(newPalayer);
newPalayer.enemies.push(player);
}
});
players.push(newPalayer)
return newPalayer
}
我们来跑一下这个游戏:
const pidan=playerFatory('皮蛋','red');
const xiaoguai=playerFatory('小乖','red');
const baobao=playerFatory('宝宝','red');
const xiaoqiang=playerFatory('小强','red');
const heiniu=playerFatory('黑妞','blue');
const congtou=playerFatory('葱头','blue');
const pangdun=playerFatory('胖墩','blue');
const haidao=playerFatory('海盗','blue');
heiniu.die();
congtou.die();
pangdun.die();
haidao.die();
执行结果如下:
现在我们已经可以随意地为游戏增加玩家或者队伍,但问题是,每个玩家和其他玩家都是紧紧耦合在一起的。在此段代码中,每个玩家对象都有两个属性,this.partners和this.enemies,用来保存对其他玩家对象的引用。当每个对象的状态发生改变,比如角色移动、吃到道具或者死亡时,都必须要显式地遍历通知其他对象。
在这个例子中只创建了8个玩家,或许还没有对你产生足够多的困扰,而如果在一个大型网络游戏中,画面里有成百上千个玩家,几十支队伍在互相厮杀。如果有一个玩家掉线,必须从所有其他玩家的队友列表和敌人列表中都移除这个玩家。游戏也许还有解除队伍和添加到别的队伍的功能,红色玩家可以突然变成蓝色玩家,这就不再仅仅是循环能够解决的问题了。
中介者模式中,Player对象可以不再执行具体方法了。而把这一切委托给一个 playerDirector
来实现。为了说明这个模式的靠谱,我们增加一个切换队伍的功能。接下来运行环境将切换到nodejs环境:
const playerDirector=require('./playerDirector');
class Player{
constructor(name,teamColor){
this.state='live' //生存状态
this.teamColor=teamColor;// 队伍颜色
this.name=name;
}
win(){
console.log(this.name,'win');
}
lose(){
console.log(this.name,'lose');
}
die(){
this.state='dead';
playerDirector.ReceiveMessage('playerDead',this);//给中介者发送消息,玩家死亡
}
remove(){
playerDirector.ReceiveMessage('remove',this);//出局
}
changeTeam(color){
playerDirector.ReceiveMessage('changeTeam',color,this);// 换队
}
}
然后工厂模式创建玩家:
const playerFatory=(name,teamColor)=>{
var newPalayer= new Player(name,teamColor);
playerDirector.ReceiveMessage('addPlayer',newPalayer);// 通知中介者增加玩家
return newPalayer;
}
这时玩家就是消息发布者,playerDirectorr开放一个对外暴露的接口ReceiveMessage,负责接收player对象发送的消息,而player对象发送消息的时候,总是把自身this作为参数发送给playerDirector,以便playerDirector识别消息来自于哪个玩家对象。
// playerDirector.js
let players = {};
// 方法库
let operations = {
// 加入
addPlayer(player) {
const { teamColor } = player;
// 如果有就调用,没有就创建
players[teamColor] = players[teamColor] || [];
players[teamColor].push(player);
},
// 出局
remove(player) {
const { teamColor } = player;
// 玩家所在队伍
const teamPlayers = players[teamColor] || [];
teamPlayers.forEach((teamPlayer, index) => {
if (teamPlayer == player) {
teamPlayer.splice(index, 1);
}
})
},
changeTeam(player, newColor) {
operations.remove(player);
player.teamColor = newColor;
operations.addPlayer(player);
},
dead(player) {
const { teamColor } = player;
// 玩家所在队伍
const teamPlayers = players[teamColor] || [];
let all_die = true;
// 第一个循环:判断队友都死光没
teamPlayers.forEach((teamPlayer)=>{
if (teamPlayer.state !== 'dead') {
all_die = false;
return
}
});
if (all_die) {
// 第二个循环:通知这队人输了。
teamPlayers.forEach((teamPlayer)=>{
teamPlayer.lose();
});
// 第三个循环:告诉其他队伍,赢了
Object.keys(players).forEach((color)=>{
if(color!==teamColor){
players[color].forEach((_player)=>{
_player.win();
});
}
})
}
}
};
const ReceiveMessage=function(){
const message=Array.prototype.shift.call(arguments);//arguments的第一个参数为消息名称
operations[message].apply(this,arguments);
}
module.exports={
ReceiveMessage
}
可以看到,除了中介者本身,没有一个玩家知道其他任何玩家的存在,玩家与玩家之间的耦合关系已经完全解除,某个玩家的任何操作都不需要通知其他玩家,而只需要给中介者发送一个消息,中介者处理完消息之后会把处理结果反馈给其他的玩家对象。我们还可以继续给中介者扩展更多功能,以适应游戏需求的不断变化。
接下来是和前端更近的例子:商城。
假设我们正在编写一个手机购买的订单页面,如下:
在购买流程中,可以选择手机的颜色以及输入购买数量,同时页面中有两个展示区域,分别向用户展示刚刚选择好的颜色和数量。还有一个按钮动态显示下一步的操作,我们需要查询该颜色手机对应的库存,如果库存数量少于这次的购买数量,按钮将被禁用并且显示库存不足,反之按钮可以点击并且显示放入购物车。
相信你在写逻辑的时候,都已经知道自己将至少和五个节点"发生关系":颜色下拉框(#color),数量input框(#number),颜色信息(.color),选择数量(.number)以及下单按钮(#submit)。
在mvvm前端框架中,都会用状态来控制这个视图的渲染,原生js应当如何控制呢?
实现方法很简单,但写出来可能很难维护。直接看中介者模式的实现方案:
// 后端返回的信息:
const data={
'blue':3,
'red':5
}
const mediator={
changed:(obj)=>{
let value=obj.value;
switch (obj.id) {
case 'color':
document.querySelector('.color').innerText=value;
break;
case 'number':
document.querySelector('.number').innerText=value;
var color=document.querySelector('#color').value;
document.querySelector('#submit').setAttribute('disabled',value>data[color]);
break;
default:
break;
}
}
}
document.querySelector('#color').addEventListener('change',(e)=>{
mediator.changed(e.target);
});
document.querySelector('#number').addEventListener('change',(e)=>{
mediator.changed(e.target);
});
如果需要增加什么关联,只要修改mediator即可。
中介者模式是迎合迪米特法则的一种实现。迪米特法则也叫最少知识原则,是指一个对象应该尽可能少地了解另外的对象(类似不和陌生人说话)。
而在中介者模式里,对象之间几乎不知道彼此的存在,它们只能通过中介者对象来互相影响对方。因此,中介者模式使各个对象之间得以解耦,以中介者和对象之间的一对多关系取代了对象之间的网状多对多关系。各个对象只需关注自身功能的实现,对象之间的交互关系交给了中介者对象来实现和维护。
中介者模式也存在一些缺点。其中,最大的缺点是系统中会新增一个中介者对象,因为对象之间交互的复杂性,转移成了中介者对象的复杂性,使得中介者对象本身经常是巨大的。中介者对象自身往往就是一个难以维护的对象。
现实中有耦合是避免不了的。毕竟我们写程序是为了快速完成项目交付生产,而不是堆砌模式和过度设计。关键就在于如何去衡量对象之间的耦合程度。一般来说,如果对象之间的复杂耦合确实导致调用和维护出现了困难,而且这些耦合度随项目的变化呈指数增长曲线,那我们就可以考虑用中介者模式来重构代码。