JavaScript設(shè)計模式之中介者模式詳解
中介者模式
在我們生活的世界中,每個人每個物體之間都會產(chǎn)生一些錯綜復(fù)雜的聯(lián)系。在應(yīng)用程序里也是一樣,程序由大大小小的單一對象組成,所有這些對象都按照某種關(guān)系和規(guī)則來通信。
平時我們大概能記住 10 個朋友的電話、30 家餐館的位置。在程序里,也許一個對象會和其他 10 個對象打交道,所以它會保持 10 個對象的引用。當(dāng)程序的規(guī)模增大,對象會越來越多,它們之間的關(guān)系也越來越復(fù)雜,難免會形成網(wǎng)狀的交叉引用。當(dāng)我們改變或刪除其中一個對象的時候,很可能需要通知所有引用到它的對象。這樣一來,就像在心臟旁邊拆掉一根毛細(xì)血管一般, 即使一點很小的修改也必須小心翼翼,如下圖所示。
面向?qū)ο笤O(shè)計鼓勵將行為分布到各個對象中,把對象劃分成更小的粒度,有助于增強(qiáng)對象的可復(fù)用性,但由于這些細(xì)粒度對象之間的聯(lián)系激增,又有可能會反過來降低它們的可復(fù)用性。
中介者模式的作用就是解除對象與對象之間的緊耦合關(guān)系。增加一個中介者對象后,所有的相關(guān)對象都通過中介者對象來通信,而不是互相引用,所以當(dāng)一個對象發(fā)生改變時,只需要通知中介者對象即可。中介者使各對象之間耦合松散,而且可以獨立地改變它們之間的交互。中介者模式使網(wǎng)狀的多對多關(guān)系變成了相對簡單的一對多關(guān)系,如下圖所示。
在前面的圖中,如果對象 A 發(fā)生了改變,則需要同時通知跟 A 發(fā)生引用關(guān)系的 B、D、E、F 這 4 個對象;而在上圖中,使用中介者模式改進(jìn)之后,A 發(fā)生改變時則只需要通知這個中介者對象即可。
現(xiàn)實中的中介者
在現(xiàn)實生活中也有很多中介者的例子,例如機(jī)場指揮塔。
中介者也被稱為調(diào)停者,我們想象一下機(jī)場的指揮塔,如果沒有指揮塔的存在,每一架飛機(jī)要和方圓 100 公里內(nèi)的所有飛機(jī)通信,才能確定航線以及飛行狀況,后果是不可想象的?,F(xiàn)實中的情況是,每架飛機(jī)都只需要和指揮塔通信。指揮塔作為調(diào)停者,知道每一架飛機(jī)的飛行狀況,所以它可以安排所有飛機(jī)的起降時間,及時做出航線調(diào)整。
下面我們來看中介者模式在下面這個案例中的應(yīng)用。
中介者模式的例子
泡泡堂游戲
大家可能都還記得泡泡堂游戲,現(xiàn)在我們來一起回顧這個游戲,假設(shè)在游戲之初只支持兩個玩家同時進(jìn)行對戰(zhàn)。
先定義一個玩家構(gòu)造函數(shù),它有 3 個簡單的原型方法:Play.prototype.win
、Play.prototype.lose
以及表示玩家死亡的 Play.prototype.die
。
因為玩家的數(shù)目是 2,所以當(dāng)其中一個玩家死亡的時候游戲便結(jié)束, 同時通知它的對手勝利。 這段代碼看起來很簡單:
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(); };
接下來創(chuàng)建 2 個玩家對象:
const player1 = new Player('玩家一'); const player2 = new Player('玩家二');
給玩家相互設(shè)置敵人:
player1.enemy = player2; player2.enemy = player1;
當(dāng)玩家 player1
被泡泡炸死的時候,只需要調(diào)用這一句代碼便完成了一局游戲:
player1.die();// 輸出:玩家一 lost、玩家二 won
然而真正的泡泡堂游戲至多可以有 8 個玩家,并分成紅藍(lán)兩隊進(jìn)行游戲。
為游戲增加隊伍
現(xiàn)在我們改進(jìn)一下游戲。因為玩家數(shù)量變多,用下面的方式來設(shè)置隊友和敵人無疑很低效:
player1.partners = [player1, player2, player3, player4]; player1.enemies = [player5, player6, player7, player8]; Player5.partners = [player5, player6, player7, player8]; Player5.enemies = [player1, player2, player3, player4];
所以我們定義一個數(shù)組 players
來保存所有的玩家,在創(chuàng)建玩家之后,循環(huán) players 來給每個玩家設(shè)置隊友和敵人:
const players = [];
再改寫構(gòu)造函數(shù) Player
,使每個玩家對象都增加一些屬性,分別是隊友列表、敵人列表 、 玩家當(dāng)前狀態(tài)、角色名字以及玩家所在的隊伍顏色:
function Player(name, teamColor) { this.partners = []; // 隊友列表 this.enemies = []; // 敵人列表 this.state = 'live'; // 玩家狀態(tài) this.name = name; // 角色名字 this.teamColor = teamColor; // 隊伍顏色 };
玩家勝利和失敗之后的展現(xiàn)依然很簡單,只是在每個玩家的屏幕上簡單地彈出提示:
Player.prototype.win = function () { // 玩家團(tuán)隊勝利 console.log('winner: ' + this.name); }; Player.prototype.lose = function () { // 玩家團(tuán)隊失敗 console.log('loser: ' + this.name); };
玩家死亡的方法要變得稍微復(fù)雜一點,我們需要在每個玩家死亡的時候,都遍歷其他隊友的生存狀況,如果隊友全部死亡,則這局游戲失敗,同時敵人隊伍的所有玩家都取得勝利,代碼如下:
Player.prototype.die = function () { // 玩家死亡 let all_dead = true; this.state = 'dead'; // 設(shè)置玩家狀態(tài)為死亡 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(); } } };
最后定義一個工廠來創(chuàng)建玩家:
const playerFactory = function (name, teamColor) { const newPlayer = new Player(name, teamColor); // 創(chuàng)建新玩家 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; };
現(xiàn)在來感受一下, 用這段代碼創(chuàng)建 8 個玩家:
//紅隊: var player1 = playerFactory('皮蛋', 'red'), player2 = playerFactory('小乖', 'red'), player3 = playerFactory('寶寶', 'red'), player4 = playerFactory('小強(qiáng)', 'red'); //藍(lán)隊: var player5 = playerFactory('黑妞', 'blue'), player6 = playerFactory('蔥頭', 'blue'), player7 = playerFactory('胖墩', 'blue'), player8 = playerFactory('海盜', 'blue');
讓紅隊玩家全部死亡:
player1.die(); player2.die(); player4.die(); player3.die();
結(jié)果如下:
loser: 寶寶
loser: 皮蛋
loser: 小乖
loser: 小強(qiáng)
winner: 黑妞
winner: 蔥頭
winner: 胖墩
winner: 海盜
玩家增多帶來的困擾
現(xiàn)在我們已經(jīng)可以隨意地為游戲增加玩家或者隊伍,但問題是,每個玩家和其他玩家都是緊緊耦合在一起的。在此段代碼中,每個玩家對象都有兩個屬性,this.partners
和 this.enemies
,用來保存其他玩家對象的引用。當(dāng)每個對象的狀態(tài)發(fā)生改變,比如角色移動、吃到道具或者死亡時,都必須要顯式地遍歷通知其他對象。
在這個例子中只創(chuàng)建了 8 個玩家,或許還沒有對你產(chǎn)生足夠多的困擾,而如果在一個大型網(wǎng)絡(luò)游戲中,畫面里有成百上千個玩家,幾十支隊伍在互相廝殺。如果有一個玩家掉線,必須從所有其他玩家的隊友列表和敵人列表中都移除這個玩家。游戲也許還有解除隊伍和添加到別的隊伍的功能,紅色玩家可以突然變成藍(lán)色玩家,這就不再僅僅是循環(huán)能夠解決的問題了。面對這樣的需求,我們上面的代碼可以迅速進(jìn)入投降模式。
用中介者模式改造泡泡堂游戲
現(xiàn)在我們開始用中介者模式來改造上面的泡泡堂游戲, 改造后的玩家對象和中介者的關(guān)系如下圖所示。
首先仍然是定義 Player
構(gòu)造函數(shù)和 player
對象的原型方法,在 player
對象的這些原型方法 中,不再負(fù)責(zé)具體的執(zhí)行邏輯,而是把操作轉(zhuǎn)交給中介者對象,我們把中介者對象命名為 playerDirector
:
function Player(name, teamColor) { this.name = name; // 角色名字 this.teamColor = teamColor; // 隊伍顏色 this.state = 'alive'; // 玩家生存狀態(tài) }; 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); // 給中介者發(fā)送消息,玩家死亡 }; /*******************移除玩家*****************/ Player.prototype.remove = function () { playerDirector.reciveMessage('removePlayer', this); // 給中介者發(fā)送消息,移除一個玩家 }; /*******************玩家換隊*****************/ Player.prototype.changeTeam = function (color) { playerDirector.reciveMessage('changeTeam', this, color); // 給中介者發(fā)送消息,玩家換隊 };
再繼續(xù)改寫之前創(chuàng)建玩家對象的工廠函數(shù),可以看到,因為工廠函數(shù)里不再需要給創(chuàng)建的玩家對象設(shè)置隊友和敵人,這個工廠函數(shù)幾乎失去了工廠的意義:
const playerFactory = function (name, teamColor) { const newPlayer = new Player(name, teamColor); // 創(chuàng)造一個新的玩家對象 playerDirector.reciveMessage('addPlayer', newPlayer); // 給中介者發(fā)送消息,新增玩家 return newPlayer; };
最后,我們需要實現(xiàn)這個中介者 playerDirector
對象,一般有以下兩種方式。
- 利用發(fā)布—訂閱模式。將
playerDirector
實現(xiàn)為訂閱者,各player
作為發(fā)布者,一旦player
的狀態(tài)發(fā)生改變,便推送消息給playerDirector
,playerDirector
處理消息后將反饋發(fā)送 給其他player
。 - 在
playerDirector
中開放一些接收消息的接口,各player
可以直接調(diào)用該接口來給playerDirector
發(fā)送消息,player
只需傳遞一個參數(shù)給playerDirector
,這個參數(shù)的目的是使playerDirector
可以識別發(fā)送者。同樣,playerDirector
接收到消息之后會將處理結(jié)果反饋給其他player
。
這兩種方式的實現(xiàn)沒什么本質(zhì)上的區(qū)別。在這里我們使用第二種方式,playerDirector
開放一個對外暴露的接口 reciveMessage
,負(fù)責(zé)接收 player
對象發(fā)送的消息,而 player
對象發(fā)送消息的時候,總是把自身 this
作為參數(shù)發(fā)送給 playerDirector
,以便 playerDirector
識別消息來自于哪個玩家對象,代碼如下:
const playerDirector = (function () { const players = {}, // 保存所有玩家 operations = {}; // 中介者可以執(zhí)行的操作 /** * 新增一個玩家 * @param {Player} player 玩家 */ operations.addPlayer = function (player) { const teamColor = player.teamColor; // 玩家的隊伍顏色 // 如果該顏色的玩家還沒有成立隊伍,則新成立一個隊伍 players[teamColor] = players[teamColor] || []; players[teamColor].push(player); // 添加玩家進(jìn)隊伍 }; /** * 移除一個玩家 * @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 的第一個參數(shù)為消息名稱 const message = Array.prototype.shift.call(arguments); operations[message].apply(this, arguments); }; return { reciveMessage } })();
可以看到,除了中介者本身,沒有一個玩家知道其他任何玩家的存在,玩家與玩家之間的耦合關(guān)系已經(jīng)完全解除,某個玩家的任何操作都不需要通知其他玩家,而只需要給中介者發(fā)送一個消息,中介者處理完消息之后會把處理結(jié)果反饋給其他的玩家對象。我們還可以繼續(xù)給中介者擴(kuò)展更多功能,以適應(yīng)游戲需求的不斷變化。
我們來看下測試結(jié)果:
// 紅隊: var player1 = playerFactory('皮蛋', 'red'), player2 = playerFactory('小乖', 'red'), player3 = playerFactory('寶寶', 'red'), player4 = playerFactory('小強(qiáng)', 'red'); // 藍(lán)隊: var player5 = playerFactory('黑妞', 'blue'), player6 = playerFactory('蔥頭', 'blue'), player7 = playerFactory('胖墩', 'blue'), player8 = playerFactory('海盜', 'blue'); player1.die(); player2.die(); player3.die(); player4.die();
運(yùn)行結(jié)果如下。
皮蛋 lost
小乖 lost
寶寶 lost
小強(qiáng) lost
黑妞 won
蔥頭 won
胖墩 won
海盜 won
假設(shè)皮蛋和小乖掉線
player1.remove(); player2.remove(); player3.die(); player4.die();
則結(jié)果如下。
寶寶 lost
小強(qiáng) lost
黑妞 won
蔥頭 won
胖墩 won
海盜 won
假設(shè)皮蛋從紅隊叛變到藍(lán)隊
player1.changeTeam( 'blue' ); player2.die(); player3.die(); player4.die();
則結(jié)果如下。
小乖 lost
寶寶 lost
小強(qiáng) lost
黑妞 won
蔥頭 won
胖墩 won
海盜 won
皮蛋 won
小結(jié)
中介者模式是迎合迪米特法則的一種實現(xiàn)。迪米特法則也叫最少知識原則,是指一個對象應(yīng)該盡可能少地了解另外的對象(類似不和陌生人說話)。如果對象之間的耦合性太高,一個對象發(fā)生改變之后,難免會影響到其他的對象,跟“城門失火,殃及池魚”的道理是一樣的。而在中介者模式里,對象之間幾乎不知道彼此的存在,它們只能通過中介者對象來互相影響對方。
因此,中介者模式使各個對象之間得以解耦,以中介者和對象之間的一對多關(guān)系取代了對象之間的網(wǎng)狀多對多關(guān)系。各個對象只需關(guān)注自身功能的實現(xiàn),對象之間的交互關(guān)系交給了中介者 對象來實現(xiàn)和維護(hù)。
不過,中介者模式也存在一些缺點。其中,最大的缺點是系統(tǒng)中會新增一個中介者對象,因為對象之間交互的復(fù)雜性,轉(zhuǎn)移成了中介者對象的復(fù)雜性,使得中介者對象經(jīng)常是巨大的。中介者對象自身往往就是一個難以維護(hù)的對象。
中介者模式可以非常方便地對模塊或者對象進(jìn)行解耦,但對象之間并非一定需要解耦。在實際項目中,模塊或?qū)ο笾g有一些依賴關(guān)系是很正常的。畢竟我們寫程序是為了快速完成項目交付生產(chǎn),而不是堆砌模式和過度設(shè)計。關(guān)鍵就在于如何去衡量對象之間的耦合程度。一般來說, 如果對象之間的復(fù)雜耦合確實導(dǎo)致調(diào)用和維護(hù)出現(xiàn)了困難,而且這些耦合度隨項目的變化呈指數(shù)增長曲線,那我們就可以考慮用中介者模式來重構(gòu)代碼。
到此這篇關(guān)于JavaScript設(shè)計模式之中介者模式詳解的文章就介紹到這了,更多相關(guān)JS中介者模式內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
ES6 系列之 Generator 的自動執(zhí)行的方法示例
這篇文章主要介紹了ES6 系列之 Generator 的自動執(zhí)行的方法示例,小編覺得挺不錯的,現(xiàn)在分享給大家,也給大家做個參考。一起跟隨小編過來看看吧2018-10-10