JavaScript設計模式之中介者模式詳解

中介者模式

在我們生活的世界中,每個人每個物體之間都會產生一些錯綜復雜的聯系。在應用程序裡也是一樣,程序由大大小小的單一對象組成,所有這些對象都按照某種關系和規則來通信。

平時我們大概能記住 10 個朋友的電話、30 傢餐館的位置。在程序裡,也許一個對象會和其他 10 個對象打交道,所以它會保持 10 個對象的引用。當程序的規模增大,對象會越來越多,它們之間的關系也越來越復雜,難免會形成網狀的交叉引用。當我們改變或刪除其中一個對象的時候,很可能需要通知所有引用到它的對象。這樣一來,就像在心臟旁邊拆掉一根毛細血管一般, 即使一點很小的修改也必須小心翼翼,如下圖所示。

面向對象設計鼓勵將行為分佈到各個對象中,把對象劃分成更小的粒度,有助於增強對象的可復用性,但由於這些細粒度對象之間的聯系激增,又有可能會反過來降低它們的可復用性。

中介者模式的作用就是解除對象與對象之間的緊耦合關系。增加一個中介者對象後,所有的相關對象都通過中介者對象來通信,而不是互相引用,所以當一個對象發生改變時,隻需要通知中介者對象即可。中介者使各對象之間耦合松散,而且可以獨立地改變它們之間的交互。中介者模式使網狀的多對多關系變成瞭相對簡單的一對多關系,如下圖所示。

在前面的圖中,如果對象 A 發生瞭改變,則需要同時通知跟 A 發生引用關系的 B、D、E、F 這 4 個對象;而在上圖中,使用中介者模式改進之後,A 發生改變時則隻需要通知這個中介者對象即可。

現實中的中介者

在現實生活中也有很多中介者的例子,例如機場指揮塔。

中介者也被稱為調停者,我們想象一下機場的指揮塔,如果沒有指揮塔的存在,每一架飛機要和方圓 100 公裡內的所有飛機通信,才能確定航線以及飛行狀況,後果是不可想象的。現實中的情況是,每架飛機都隻需要和指揮塔通信。指揮塔作為調停者,知道每一架飛機的飛行狀況,所以它可以安排所有飛機的起降時間,及時做出航線調整。

下面我們來看中介者模式在下面這個案例中的應用。

中介者模式的例子

泡泡堂遊戲

大傢可能都還記得泡泡堂遊戲,現在我們來一起回顧這個遊戲,假設在遊戲之初隻支持兩個玩傢同時進行對戰。

先定義一個玩傢構造函數,它有 3 個簡單的原型方法:Play.prototype.winPlay.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();
};

接下來創建 2 個玩傢對象:

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.partnersthis.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 的狀態發生改變,便推送消息給 playerDirectorplayerDirector 處理消息後將反饋發送 給其他 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中介者模式內容請搜索WalkonNet以前的文章或繼續瀏覽下面的相關文章希望大傢以後多多支持WalkonNet!

推薦閱讀: