JavaScript设计模式之中介者模式详解

作者:半夏

时间:2026-10-25

浏览:590

本文深入解析JavaScript中的中介者模式,旨在解决对象间复杂网状耦合问题。通过泡泡堂游戏从简单对战到多队竞技的演进案例,展示如何引入中介者对象统一管理玩家交互。文章详细对比了直接引用与中介者模式的优劣,探讨了发布-订阅与接口调用两种实现方式,并分析了该模式在降低模块耦合度与增加中介者复杂度之间的权衡。

文章目录

中介者模式

现实世界中,个体与物体间存在错综复杂的联系。在软件应用中,程序由众多单一对象构成,这些对象依据特定关系和规则进行通信。

人类通常能记住约10位朋友的电话和30家餐馆的位置。在程序中,一个对象可能需要与另外10个对象交互,从而保持这10个对象的引用。随着程序规模扩大,对象数量激增,关系网变得愈发复杂,形成网状交叉引用。此时,若修改或删除某个对象,往往需要通知所有引用它的对象。这种紧密耦合使得微小的修改也需小心翼翼,如同在心脏旁拆除毛细血管,具体结构可见下图。

中介者模式

面向对象设计提倡将行为分散到各个对象中,通过细化对象粒度来增强可复用性。然而,细粒度对象间联系激增,反而可能降低其可复用性。

中介者模式的核心作用是解除对象间的紧耦合。引入中介者对象后,所有相关对象均通过中介者进行通信,而非直接互相引用。因此,当某个对象状态改变时,只需通知中介者即可。该模式使各对象间耦合松散,允许独立修改交互逻辑,将网状的多对多关系简化为相对简单的一对多关系,具体结构可见下图。

中介者模式

在前述图中,若对象A发生改变,需同时通知与其有引用关系的B、D、E、F共4个对象;而采用中介者模式改进后,A发生改变时仅需通知中介者对象即可。

现实中的中介者

现实生活中存在诸多中介者实例,例如机场指挥塔。

中介者亦称调停者。试想若无指挥塔,每架飞机需与方圆100公里内的所有飞机通信以确认航线和状况,后果不堪设想。实际情况是,每架飞机仅需与指挥塔通信。指挥塔作为调停者,掌握所有飞机的飞行状况,从而能安排起降时间并及时调整航线。

接下来,我们将通过具体案例探讨中介者模式的应用。

中介者模式的例子

泡泡堂游戏

回顾经典的泡泡堂游戏,假设游戏初期仅支持两名玩家对战。

首先定义玩家构造函数,其包含三个简单的原型方法:Play.prototype.win、Play.prototype.lose以及表示玩家死亡的Play.prototype.die。

由于玩家数量固定为2,当其中一名玩家死亡时游戏结束,并通知对手胜利。代码如下:

function Player(name) {
	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();
};

接下来创建两个玩家对象:

const player1 = new Player('玩家一');
const player2 = new Player('玩家二');

为玩家相互设置敌人:

player1.enemy = player2; 
player2.enemy = player1;

当玩家player1被泡泡炸死时,只需调用此代码即可完成一局游戏:

player1.die();// 输出:玩家一 lost、玩家二 won 

然而,真正的泡泡堂游戏最多支持8名玩家,并分为红蓝两队进行。

为游戏增加队伍

现在改进游戏。由于玩家数量增加,若采用以下方式设置队友和敌人,显然效率低下:

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数组,为每个玩家设置队友和敌人:

const players = []; 

同时改写构造函数Player,使每个玩家对象增加若干属性,包括队友列表、敌人列表、玩家当前状态、角色名字以及玩家所在的队伍颜色:

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 () { // 玩家死亡
	let all_dead = true;
	this.state = 'dead'; // 设置玩家状态为死亡
	for (let i = 0; i < this.partners.length; i++) { // 遍历队友列表
		if (this.partners[i].state !== 'dead') { // 如果还有一个队友没有死亡,则游戏还未失败
			all_dead = false;
			break;
		}
	}
	if (all_dead === true) { // 如果队友全部死亡
		this.lose(); // 通知自己游戏失败
		for (let i = 0; i < this.partners.length; i++) { // 通知所有队友玩家游戏失败
			this.partners[i].lose();
		}
		for (let i = 0; i < this.enemies.length; i++) { // 通知所有敌人游戏胜利
			this.enemies[i].win();
		}
	}
};

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

const playerFactory = function (name, teamColor) {
	const newPlayer = new Player(name, teamColor); // 创建新玩家
	for (let i = 0; i < players.length; i++) { // 通知所有的玩家,有新角色加入
		if (players[i].teamColor === newPlayer.teamColor) { // 如果是同一队的玩家
			players[i].partners.push(newPlayer); // 相互添加到队友列表
			newPlayer.partners.push(players[i]);
		} else {
			players[i].enemies.push(newPlayer); // 相互添加到敌人列表
			newPlayer.enemies.push(players[i]);
		}
	}
	players.push(newPlayer);
	return newPlayer;
};

现在体验一下,使用此代码创建8名玩家:

//红队:
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();

结果如下:

loser: 宝宝 loser: 皮蛋 loser: 小乖 loser: 小强 winner: 黑妞 winner: 葱头 winner: 胖墩 winner: 海盗

玩家增多带来的困扰

目前我们可以随意增加玩家或队伍,但问题在于,每个玩家与其他玩家紧密耦合。在此段代码中,每个玩家对象包含this.partners和this.enemies两个属性,用于保存其他玩家对象的引用。当对象状态改变(如角色移动、获取道具或死亡)时,必须显式遍历并通知其他对象。

本例仅创建了8名玩家,或许尚未造成足够困扰。但在大型网络游戏中,画面中可能有成百上千名玩家和几十支队伍互相厮杀。若有玩家掉线,必须从所有其他玩家的队友列表和敌人列表中移除该玩家。游戏可能还包含解除队伍或加入其他队伍的功能,例如红色玩家突然变为蓝色玩家,这已非简单循环所能解决。面对此类需求,上述代码将迅速陷入维护困境。

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

现在开始使用中介者模式改造上述泡泡堂游戏。改造后的玩家对象与中介者关系如下图所示。

中介者模式

首先仍定义Player构造函数和player对象的原型方法。在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); // 给中介者发送消息,玩家换队
};

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

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

最后,我们需要实现这个中介者playerDirector对象,通常有以下两种方式。

  • 利用发布—订阅模式。将playerDirector实现为订阅者,各player作为发布者。一旦player的状态发生改变,便推送消息给playerDirector,playerDirector处理消息后将反馈发送给其他player。
  • 在playerDirector中开放一些接收消息的接口,各player可直接调用该接口给playerDirector发送消息。player只需传递一个参数给playerDirector,该参数旨在使playerDirector能够识别发送者。同样,playerDirector接收到消息后会将处理结果反馈给其他player。

这两种实现方式并无本质区别。此处我们采用第二种方式,playerDirector开放一个对外暴露的接口reciveMessage,负责接收player对象发送的消息。而player对象在发送消息时,总是将自身this作为参数发送给playerDirector,以便playerDirector识别消息来源于哪个玩家对象,代码如下:

const playerDirector = (function () {
	const players = {}, // 保存所有玩家
		operations = {}; // 中介者可以执行的操作
	/**
	 * 新增一个玩家
	 * @param {Player} player 玩家
	 */
	operations.addPlayer = function (player) {
		const teamColor = player.teamColor; // 玩家的队伍颜色
		// 如果该颜色的玩家还没有成立队伍,则新成立一个队伍
		players[teamColor] = players[teamColor] || [];
		players[teamColor].push(player); // 添加玩家进队伍
	};
	/**
	 * 移除一个玩家
	 * @param {Player} player 玩家
	 */
	operations.removePlayer = function (player) {
		const teamColor = player.teamColor, // 玩家的队伍颜色
			teamPlayers = players[teamColor] || []; // 该队伍所有成员
		for (let i = teamPlayers.length - 1; i >= 0; i--) { // 遍历删除
			if (teamPlayers[i] === player) {
				teamPlayers.splice(i, 1);
			}
		}
	};
	/**
	 * 玩家换队
	 * @param {Player} player 玩家
	 * @param {string} newTeamColor 队伍颜色
	 */
	operations.changeTeam = function (player, newTeamColor) { // 玩家换队
		operations.removePlayer(player); // 从原队伍中删除
		player.teamColor = newTeamColor; // 改变队伍颜色
		operations.addPlayer(player); // 增加到新队伍中
	};
	/**
	 * 玩家死亡
	 * @param {Player} player 玩家
	 */
	operations.playerDead = function (player) {
		const teamColor = player.teamColor,
			teamPlayers = players[teamColor]; // 玩家所在队伍
		let all_dead = true;
		for (let i = 0; i < teamPlayers.length; i++) {
			if (teamPlayers[i].state !== 'dead') {
				all_dead = false;
				break;
			}
		}
		if (all_dead) { // 全部死亡
			for (let i = 0; i < teamPlayers.length; i++) {
				teamPlayers[i].lose(); // 本队所有玩家 lose 
			}
			for (const color in players) {
				if (color !== teamColor) {
					const teamPlayers = players[color]; // 其他队伍的玩家
					for (let i = 0; i < teamPlayers.length; i++) {
						teamPlayers[i].win(); // 其他队伍所有玩家 win 
					}
				}
			}
		}
	};
	const reciveMessage = function () {
		// arguments 的第一个参数为消息名称
		const message = Array.prototype.shift.call(arguments); 
		operations[message].apply(this, arguments);
	};
	return {
		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();

运行结果如下。

皮蛋 lost 小乖 lost 宝宝 lost 小强 lost 黑妞 won 葱头 won 胖墩 won 海盗 won

假设皮蛋和小乖掉线

player1.remove(); 
player2.remove(); 
player3.die(); 
player4.die(); 

则结果如下。

宝宝 lost 小强 lost 黑妞 won 葱头 won 胖墩 won 海盗 won

假设皮蛋从红队叛变到蓝队

player1.changeTeam( 'blue' ); 
player2.die(); 
player3.die(); 
player4.die(); 

则结果如下。

小乖 lost 宝宝 lost 小强 lost 黑妞 won 葱头 won 胖墩 won 海盗 won 皮蛋 won

小结

中介者模式是迎合迪米特法则的一种实现。迪米特法则又称最少知识原则,指一个对象应尽可能少地了解其他对象(类似“不和陌生人说话”)。若对象间耦合性过高,一个对象的改变难免影响其他对象,这与“城门失火,殃及池鱼”的道理相同。而在中介者模式中,对象间几乎互不知晓,仅能通过中介者对象互相影响。

因此,中介者模式使各个对象得以解耦,以中介者与对象间的一对多关系取代了对象间的网状多对多关系。各个对象只需关注自身功能实现,对象间的交互关系交由中介者对象实现和维护。

不过,中介者模式也存在一些缺点。其中最大缺点是系统中会新增一个中介者对象。由于对象间交互的复杂性转移至中介者对象,导致中介者对象往往非常庞大,其自身可能成为一个难以维护的对象。

中介者模式可方便地对模块或对象进行解耦,但对象间并非一定需要解耦。在实际项目中,模块或对象间存在一定依赖关系是正常的。毕竟编程旨在快速完成项目交付生产,而非堆砌模式和过度设计。关键在于如何衡量对象间的耦合程度。一般而言,若对象间复杂耦合确实导致调用和维护困难,且耦合度随项目变化呈指数增长,则可考虑使用中介者模式重构代码。

到此这篇关于JavaScript设计模式之中介者模式详解的文章就介绍到这了,更多相关JS中介者模式内容请搜索本站以前的文章或继续浏览下面的相关文章希望大家以后多多支持本站!

实用工具推荐

电子包浆

绿化、压缩、噪点和虚拟水印,自由调节做旧程度。

使用 ›
图片压缩

选择画质和输出格式,预览压缩前后的大小。

使用 ›
相关文章