概念
中介者模式的作用解除对象与对象之间的紧耦合关系,增加一个中介者对象后,所有的相关对象都通过中介者对象来通信,而不是互相引用,当一个对象发生改变时,只需要通知中介者对象即可。中介者使各对象之间耦合松散,而且可以独立地改变它们之间的交互。中介者模式使网状的多对多关系变成了相对简单的一对多关系。
描述
现实生活中有很多中介者的例子。如:在世界杯期间购买足球彩票,上千万的人一起计算赔率和输赢是不可能的,博彩公司作中介,每个人只需要与博彩公司发生关联,博彩公司会根据所有人的投注情况计算好赔率,彩民们赢了钱就从博彩公司拿,输了钱就把钱交给博彩公司。
应用
我们来用JS模拟泡泡堂游戏。
【初始版本】
Play.prototype.win
、Play.prototype.lose
表示玩家输赢以及表示玩家死亡Play.prototype.die
。因为玩家的数目是2,所以当其中一个玩家死亡的时候游戏便结束,同时通知它的对手胜利。
class Player {
constructor(name) {
this.name = name
this.enemy = null
}
win() {
console.log(this.name + ' won ')
}
lose() {
console.log(this.name + ' lost')
}
die() {
this.lose()
this.enemy.win()
}
}
接下来创建2个玩家对象:
let player1 = new Player( 'hello' )
let player2 = new Player( 'world' )
给玩家相互设置敌人:
player1.enemy = player2;
player2.enemy = player1;
当玩家player1被泡泡炸死的时候,只需要调用这一句代码便完成了一局游戏:
player1.die()
上面代码只是2个玩家,真正的泡泡堂游戏可以有8个玩家,并分成红蓝两队进行游戏。
定义一个数组players来保存所有的玩家,在创建玩家之后,循环players来给每个玩家设置队友和敌人:
let players = []
为每个玩家对象都增加一些属性,分别是队友列表、敌人列表、玩家当前状态、角色名字以及玩家所在的队伍颜色:
class Player {
constructor(name, teamColor) {
this.partners = [] // 队友列表
this.enemies = [] // 敌人列表
this.state = 'live' // 玩家状态
this.name = name // 角色名字
this.teamColor = teamColor // 队伍颜色
}
win() { // 玩家团队胜利
console.log( 'winner: ' + this.name )
}
lose() { // 玩家团队失败
console.log( 'loser: ' + this.name )
}
die() { // 玩家死亡的方法要变得稍微复杂一点,需要在每个玩家死亡的时候,都遍历其他队友的生存状况,
// 如果队友全部死亡,则这局游戏失败,同时敌人队伍的所有玩家都取得胜利
let all_dead = true
this.state = 'dead' // 设置玩家状态为死亡
for ( let i = 0, partner; partner = this.partners[ i++ ]; ){ // 遍历队友列表
if ( partner.state !== 'dead' ){ // 如果还有一个队友没有死亡,则游戏还未失败
all_dead = false
break
}
}
if ( all_dead === true ){ // 如果队友全部死亡
this.lose() // 通知自己游戏失败
for ( let i = 0, partner; partner = this.partners[ i++ ]; ){ // 通知所有队友玩家游戏失败
partner.lose()
}
for ( let i = 0, enemy; enemy = this.enemies[ i++ ]; ){ // 通知所有敌人游戏胜利
enemy.win()
}
}
}
}
定义一个工厂来创建玩家:
let playerFactory = function( name, teamColor ){
let newPlayer = new Player( name, teamColor ) // 创建新玩家
for ( let 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
}
//红队:
let player1 = playerFactory( '北京', 'red' ),
player2 = playerFactory( '天津', 'red' ),
player3 = playerFactory( '郑州', 'red' ),
player4 = playerFactory( '青岛', 'red' )
//蓝队:
let player5 = playerFactory( '上海', 'blue' ),
player6 = playerFactory( '深圳', 'blue' ),
player7 = playerFactory( '杭州', 'blue' ),
player8 = playerFactory( '广州', 'blue' )
现在已经可以随意地为游戏增加玩家或者队伍,但问题是,每个玩家和其他玩家都是紧紧耦合在一起的。在此段代码中,每个玩家对象都有两个属性,this.partners
和this.enemies
,用来保存其他玩家对象的引用。当每个对象的状态发生改变,比如角色移动、吃到道具或者死亡时,都必须要显式地遍历通知其他对象。
如果在一个大型网络游戏中,画面里有成百上千个玩家,几十支队伍在互相厮杀。如果有一个玩家掉线,必须从所有其他玩家的队友列表和敌人列表中都移除这个玩家。游戏也许还有解除队伍和添加到别的队伍的功能,红色玩家可以突然变成蓝色玩家,这就不再仅仅是循环能够解决的问题了。
【中介者模式】
现在开始用中介者模式来改造上面的泡泡堂游戏,首先仍然是定义Player构造函数和player对象的原型方法,在player对象的这些原型方法中,不再负责具体的执行逻辑,而是把操作转交给中介者对象,把中介者对象命名为playerDirector:
class Player {
constructor(name, teamColor) {
this.name = name
this.teamColor = teamColor
this.state = 'live'
}
win() {
console.log( this.name + ' won ' )
}
lose() {
console.log( this.name +' lost' )
}
die() {
this.state = 'dead'
playerDirector.reciveMessage( 'playerDead', this ) // 给中介者发送消息,玩家死亡
}
remove() {
playerDirector.reciveMessage( 'removePlayer', this ) // 给中介者发送消息,移除一个玩家
}
changeTeam(color) {
playerDirector.reciveMessage( 'changeTeam', this, color ) // 给中介者发送消息,玩家换队
}
}
再继续改写之前创建玩家对象的工厂函数,可以看到,因为工厂函数里不再需要给创建的玩家对象设置队友和敌人,这个工厂函数几乎失去了工厂的意义:
let playerFactory=function(name,teamColor){
let newPlayer=new Player(name,teamColor) //创造一个新的玩家对象
playerDirector.reciveMessage('addPlayer',newPlayer) //给中介者发送消息,新增玩家
return newPlayer
}
playerDirector开放一个对外暴露的接口reciveMessage,负责接收player对象发送的消息,而player对象发送消息的时候,总是把自身this作为参数发送给playerDirector,以便playerDirector识别消息来自于哪个玩家对象。
let playerDirector= ( function(){
let players = {}, // 保存所有玩家
operations = {} // 中介者可以执行的操作
/****************新增一个玩家***************************/
operations.addPlayer = function( player ){
let teamColor = player.teamColor // 玩家的队伍颜色
players[ teamColor ] = players[ teamColor ] || [] // 如果该颜色的玩家还没有成立队伍,则新成立一个队伍
players[ teamColor ].push( player ) // 添加玩家进队伍
}
/****************移除一个玩家***************************/
operations.removePlayer = function( player ){
let teamColor = player.teamColor, // 玩家的队伍颜色
teamPlayers = players[ teamColor ] || [] // 该队伍所有成员
for ( let 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 ){ // 玩家死亡
let teamColor = player.teamColor,
teamPlayers = players[ teamColor ] // 玩家所在队伍
let all_dead = true
for ( let i = 0, player; player = teamPlayers[ i++ ]; ){
if ( player.state !== 'dead' ){
all_dead = false
break
}
}
if ( all_dead === true ){ // 全部死亡
for ( let i = 0, player; player = teamPlayers[ i++ ]; ){
player.lose() // 本队所有玩家lose
}
for ( let color in players ){
if ( color !== teamColor ){
let teamPlayers = players[ color ] // 其他队伍的玩家
for ( let i = 0, player; player = teamPlayers[ i++ ]; ){
player.win() // 其他队伍所有玩家win
}
}
}
}
}
let reciveMessage = function(){
let message = Array.prototype.shift.call( arguments ) // arguments 的第一个参数为消息名称
operations[ message ].apply( this, arguments )
}
return {
reciveMessage: reciveMessage
}
})()
可以看到,除了中介者本身,没有一个玩家知道其他任何玩家的存在,玩家与玩家之间的耦合关系已经完全解除,某个玩家的任何操作都不需要通知其他玩家,而只需要给中介者发送一个消息,中介者处理完消息之后会把处理结果反馈给其他的玩家对象。还可以继续给中介者扩展更多功能,以适应游戏需求的不断变化。
小结
中介者模式符合迪米特法。迪米特法则也叫最少知识原则,是指一个对象应该尽可能少地了解另外的对象。如果对象之间的耦合性太高,一个对象发生改变之后,难免会影响到其他的对象。而在中介者模式里,对象之间几乎不知道彼此的存在,它们只能通过中介者对象来互相影响对方。因此,中介者模式使各个对象之间得以解耦,以中介者和对象之间的一对多关系取代了对象之间的网状多对多关系。各个对象只需关注自身功能的实现,对象之间的交互关系交给了中介者对象来实现和维护。不过,中介者模式也存在一些缺点。其中,最大的缺点是系统中会新增一个中介者对象,因为对象之间交互的复杂性,转移成了中介者对象的复杂性,使得中介者对象经常是巨大的。中介者对象自身往往就是一个难以维护的对象。
参考文献
《JavaScript设计模式与开发实践》