在咱們的生活的世界中,每一個人每一個物體之間都會產生一些錯綜複雜的聯繫。在應用程序裏也是同樣,程序由大大小小的單一對象組成,全部這些對象都按照某種關係和規則來通訊。html
平時咱們大概能記住 10 個朋友的電話,30 家餐館的位置(打比方)。在程序裏,也許一個對象會和其餘 10 個對象打交道,因此它會保持 10 個對象的引用。當程序的規模增大,對象會愈來愈多,它們之間的關係也就愈來愈複雜,不免會造成網狀的交叉引用。當咱們改變或刪除其中一個對象的時候,極可能須要通知全部引用到它的對象。這樣一來,就像在心臟旁邊拆掉一根毛細血管通常,即便一點很小的修改也必須當心翼翼。設計模式
面向對象設計鼓勵將行爲分佈到各個對象中,把對象劃分紅更小的粒度,有助於加強對象的可複用性,但因爲這些細粒度對象之間的聯繫激增,又有可能會反過來下降它們的可複用性。數組
中介者模式的做用就是解除對象與對象之間的緊耦合關係。增長一箇中介者對象後,全部的相關對象都經過中介者對象來通訊,而不是相互引用,因此當一個對象發生改變時,只須要通知中介者對象便可。中介者使各對象之間耦合鬆散,並且能夠獨立地改變它們之間的交互。中介者模式使用網狀的多對多關係變成了相對簡單的一對多關係。ruby
在現實生活中也有不少中介者的例子,下面例舉幾個:網絡
你們可能都還記得泡泡堂遊戲,我(本書做者)曾經寫過一個 JS 版的泡泡堂,如今咱們來一塊兒回顧這個遊戲,在遊戲之初只支持兩個玩家同時進行對戰。app
先定義一個玩家構造函數,它有 3 個簡單的原型方法: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(); }
接下來建立 2 個玩家對象:this
var player1 = new Player('皮蛋'); var player2 = new Player('小乖');
給玩家相互設置敵人:prototype
player1.enemy = player2; player2.enemy = player1;
當玩家 player1 被泡泡炸死的時候,只須要掉用這一句代碼便結束遊戲:
player1.die(); //輸出:皮蛋 lost 小乖 won
咱們定義一個數組 players 來保存全部的玩家,在建立玩家以後,循環 players 來給每一個玩家設置隊友和敵人:
var 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 () { //玩家死亡 var all_dead = true; this.state = 'dead'; //設置玩家狀態爲死亡 for (var i = 0, partner; partner = this.partners[i++]; ){ //遍歷隊友列表 if (partner.state !== 'dead') { //若是還有一個隊友沒有死亡,則遊戲還未失敗 all_dead = false; break; } } if (all_dead === true){ //若是隊友所有死亡 this.lose(); //通知本身遊戲失敗 for (var i = 0, partner; partner = this.partners[i++]; ) { //通知全部隊友玩家遊戲失敗 partner.lose(); } for (var i = 0, enemy; enemy = this.enemies[i++]; ){ //通知全部敵人遊戲勝利 enemy.win(); } } };
最後定義一個工廠來建立玩家:
function playerFactory (name, teamColor) { var newPlayer = new Player(name, teamColor); //建立新玩家 for (var 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; };
如今來感覺一下,用這段代碼建立 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();
如今咱們已經能夠隨意地爲遊戲增長玩家或者隊伍,但問題是,每一個玩家和其餘玩家都是牢牢耦合在一塊兒的。在此段代碼中,每一個玩家都有兩個屬性,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 + ' lose'); }; //死亡 Player.prototype.die = function () { this.state = 'dead'; playerDirector.ReceiveMessage('playerDead', this); //給中介者發送玩家死亡信息 }; //移除 Player.prototype.remove = function () { playerDirector.ReceiveMessage('removePlayer', this) //給中介者發送移除玩家消息 }; //換隊 Player.prototype.changeTeam = function (color) { playerDirector.ReceiveMessage('changeTeam', this, color); //給中介者發送玩家換隊消息 }
在繼續改寫以前建立玩家對象的工廠函數,能夠看到,由於工廠函數裏再也不須要給建立的玩家對象設置隊友和敵人,這個工廠函數幾乎失去了工廠的意義:
var playerFactory = function (name, teamColor) { var newPlayer = new Player(name, teamColor); //建立一個玩家 playerDirector.ReceiveMessage('addPlayer', newPlayer); //給中介者發送新增玩家消息 return newPlayer; };
最後,咱們須要實現這個中介者 PlayerDirector 對象,通常有如下兩種方式:
這兩種方式的實現沒有本質上的區別。在這裏咱們使用第二種方式,playerDirector 開放一個對外暴露的接口 ReceiveMessage ,負責接收 player 對象發送的消息,而 player 對象發送消息的時候,老是把自身 this 做爲參數發送給 playerDirector ,以便 playerDirector 識別消息來自於哪一個玩家對象,代碼以下:
var playerDirector = (function () { var players = {}, //保存全部玩家 operations = {}; //中介者能夠執行的操做 //新增玩家 operations.addPlayer = function (player) { var teamColor = player.teamColor; //玩家隊伍顏色 players[teamColor] = players[teamColor] || []; //若是該顏色的玩家尚未成立隊伍,則成立隊伍 players[teamColor].push(player); //添加玩家入隊 }; //移除玩家 operations.removePlayer = function (player) { var teamColor = player.teamColor, //玩家隊伍顏色 teamPlayers = players[teamColor] || []; //該隊伍全部成員 for (var 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) { var teamColor = player.teamColor, teamPlayers = players[teamColor]; //玩家所在隊伍 var all_dead = true; for (var i = 0, player; player = teamPlayers[i++]; ) { if (player.state !== 'dead') { all_dead = false; break; } } if (all_dead === true) { //所有死亡 for (var i = 0, player; player = teamPlayers[i++]; ) { player.lose(); //通知本隊全部玩家失敗 } for (var color in players) { //遍歷全部玩家 if (color !== teamColor) { var teamPlayers = players[color]; //其餘隊伍玩家 for (var i = 0, player; player = teamPlayers[i++]; ) { player.win(); //通知其餘隊伍全部玩家勝利 } } } } }; var ReceiveMessage = function () { var message = Array.prototype.shift.call(arguments); // arguments 的第一個參數爲消息名稱 operations[message].apply(this, arguments); }; return { ReceiveMessage: ReceiveMessage } })();
能夠看到,除了中介者自己,沒有一個玩家知道其餘任何玩家的存在,玩家與玩家之間的耦合關係已經徹底解除,某個玩家的任何操做都不須要通知其餘玩家,而只須要給中介者發送一個消息,中介者處理完消息以後會把處理結果反饋給其餘的玩家對象。咱們還能夠繼續給中介者擴展更多的功能,以適應遊戲需求的不斷變化。
咱們來看下測試結果:
//紅隊 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();
假設皮蛋和小乖掉線:
player1.remove(); player2.remove(); player3.die(); player4.die();
假設皮蛋從紅隊叛變到藍隊:
player1.changeTeam('blue'); player2.die(); player3.die(); player4.die();
假設咱們正在編寫一個手機購買的頁面,在購買流程中,能夠選擇手機的顏色以及輸入購買數量,同時頁面中有兩個展現區域,分別向用戶展現剛剛選擇好的顏色和數量。還有一個按鈕動態顯示下一步的操做,咱們須要查詢該顏色手機對應的庫存,若是庫存數量少於此次的購買數量,按鈕將被禁用而且顯示庫存不足,反之按鈕能夠點擊而且顯示放入購物車。
這個需求是很是容易實現的,假設咱們已經提早從後臺獲取到了全部顏色手機的庫存量:
var goods = { //手機庫存 "red": 3, "blue": 6 };
那麼頁面有可能顯示爲以下幾種場景:
選擇紅色手機,購買 4 個,庫存不足。
選擇藍色手機,購買 5 個,庫存充足,能夠加入購物車。
或者是沒有輸入購買數量的時候,按鈕將被禁用並顯示相應提示。
咱們大概已經可以猜到,接下來將遇到至少 5 個節點,分別是:
咱們從編寫 HTML 代碼開始:
選擇顏色: <select id = "colorSelect"> <option value = "">請選擇</option> <option value = "red">紅色</option> <option value = "blue">藍色</option> </select>> 輸入購買數量: <input type = "text" id = "numberInput" /><br /><br /> 你選擇的顏色: <div id = "colorInfo"></div><br /> 你輸入的數量: <div id = "numberInfo"></div><br /> <button id = "nextBtn" disable = "true">請選擇手機顏色和購買數量</button>
接下來分別監聽 colorSelect 的 onchange 事件函數和 numberInput 的 oninput 事件函數,而後在這兩個事件中做出相應處理。
var colorSelect = document.getElementById("colorSelect"), numberInput = document.getElementById("numberInput"), colorInfo = document.getElementById("colorInfo"), numberInfo = document.getElementById("numberInfo"), nextBtn = document.getElementById("nextBtn"); var goods = { //手機庫存 "red": 3, "blue": 6 }; colorSelect.onchange = function () { var color = this.value, //顏色 number = numberInput.value, //數量 stock = goods[color]; //該顏色手機對應的當前庫存 colorInfo.innerHTML = color; if (!color) { nextBtn.disabled = true; nextBtn.innerHTML = '請選擇手機顏色'; return; } if (((number - 0) | 0) !== number - 0) { //用戶輸入的購買數量是否爲正整數。關於按位或操做符(|),能夠觀看這篇博客 http://www.cnblogs.com/rubylouvre/p/3183616.html nextBtn.disabled = true; nextBtn.innerHTML = '請輸入正確的購買數量'; return; } if (number > stock) { //當前選擇的數量超過庫存量時 nextBtn.disabled = true; nextBtn.innerHTML = '庫存不足'; return; } nextBtn.disabled = false; nextBtn.innerHTML = '放入購物車'; };
來考慮一下,當觸發了 colorSelect 的 onchange 以後,會發生什麼事情。
首先咱們要讓 colorInfo 中顯示當前選中的顏色,而後獲取用戶當前輸入的購買數量,對用戶的輸入值進行一些合法性判斷。再根據庫存數量來判斷 nextBtn 的顯示狀態。
別忘了,咱們還要編寫 numberInput 的事件相關代碼:
numberInput.oninput = function () { var color = colorSelect.value, number = this.value, stock = goods[color]; numberInfo.innerHTML = number; if (!color) { nextBtn.disabled = true; nextBtn.innerHTML = '請選擇手機顏色'; return; } if (((number - 0) | 0) !== number - 0) { nextBtn.disabled = true; nextBtn.innerHTML = '請輸入正確的購買數量'; return; } if (number > stock) { nextBtn.disabled = true; nextBtn.innerHTML = '庫存不足'; return; } nextBtn.disabled = false; nextBtn.innerHTML = '放入購物車'; }
雖然目前順利完成了代碼編寫,但隨之而來的需求改變有可能給咱們帶來麻煩。假設如今要求去掉 colorInfo 和 numberInfo 這兩個展現區域,咱們就要分別改動 colorSelect.onchange 和 numberInput.oninput 裏面的代碼,由於在先前的代碼中,這些對象確實是耦合在一塊兒的。
目前咱們面臨的對象還不算太多,當這個頁面裏的節點激增到 10 個或者 15 個時,它們之間的聯繫可能變得更加錯綜複雜,任何一次改動都將變得很棘手。爲了證明這一點,咱們假設頁面中將新增另一個下拉選擇框,表明選擇手機內存。如今咱們須要計算顏色,內存和購買數量,來判斷 nextBtn 是顯示庫存不足仍是放入購物車。
首先咱們要增長兩個 HTML 節點:
選擇顏色: <select id = "colorSelect"> <option value = "">請選擇</option> <option value = "red">紅色</option> <option value = "blue">藍色</option> </select> 選擇內存: <select id = "memorySelect"> <option value = "">請選擇</option> <option value = "32G">32G</option> <option value = "16G">16G</option> </select> 輸入購買數量: <input type = "text" id = "numberInput" /><br /><br /> 你選擇的顏色: <div id = "colorInfo"></div><br /> 你選擇的內存: <div id = "memoryInfo"></div><br /> 你輸入的數量: <div id = "numberInfo"></div><br /> <button id = "nextBtn" disable = "true">請選擇手機顏色和購買數量</button>
接下來修改表示庫存的 JSON 對象以及修改 colorSelect 的 onchange 事件函數和 numberInput 的 oninput 事件函數:
colorSelect.onchange = function () { var color = this.value, memory = memorySelect.value; //內存 number = numberInput.value, stock = goods[color + '|' + memory]; //該顏色與內存對應的手機庫存 colorInfo.innerHTML = color; if (!color) { nextBtn.disabled = true; nextBtn.innerHTML = '請選擇手機顏色'; return; } if (!memory) { nextBtn.disabled = true; nextBtn.innerHTML = '請選擇內存大小'; return; } if (((number - 0) | 0) !== number - 0) { nextBtn.disabled = true; nextBtn.innerHTML = '請輸入正確的購買數量'; return; } if (number > stock) { nextBtn.disabled = true; nextBtn.innerHTML = '庫存不足'; return; } nextBtn.disabled = false; nextBtn.innerHTML = '放入購物車'; }; numberInput.oninput = function () { var color = colorSelect.value, memory = memorySelect.value; number = this.value, stock = goods[color + '|' + memory]; numberInfo.innerHTML = number; if (!color) { nextBtn.disabled = true; nextBtn.innerHTML = '請選擇手機顏色'; return; } if (!memory) { nextBtn.disabled = true; nextBtn.innerHTML = '請選擇內存大小'; return; } if (((number - 0) | 0) !== number - 0) { nextBtn.disabled = true; nextBtn.innerHTML = '請輸入正確的購買數量'; return; } if (number > stock) { nextBtn.disabled = true; nextBtn.innerHTML = '庫存不足'; return; } nextBtn.disabled = false; nextBtn.innerHTML = '放入購物車'; }
最後還要新增 memorySelect 的 onchange 事件函數:
memorySelect.onchange = function () { var color = colorSelect.value, memory = this.value, number = numberInput.value, stock = goods[color + '|' + memory]; memoryInfo.innerHTML = memory; if (!color) { nextBtn.disabled = true; nextBtn.innerHTML = '請選擇手機顏色'; return; } if (!memory) { nextBtn.disabled = true; nextBtn.innerHTML = '請選擇內存大小'; return; } if (((number - 0) | 0) !== number - 0) { nextBtn.disabled = true; nextBtn.innerHTML = '請輸入正確的購買數量'; return; } if (number > stock) { nextBtn.disabled = true; nextBtn.innerHTML = '庫存不足'; return; } nextBtn.disabled = false; nextBtn.innerHTML = '放入購物車'; }
很遺憾,咱們僅僅是增長一個內存的選擇條件,就要改變如此多的代碼,這是由於在目前的實現中,每一個節點對象都是耦合在一塊兒的,改變或者增長任何一個節點對象,都要通知到與其相關的對象。
如今咱們來引入中介者對象,全部的節點對象只跟中介者通訊。當下拉選擇框 colorSelect,memorySelect 和文本輸入框 numberInput 發生了事件行爲時,它們僅僅通知中介者它們改變了,同時把自身看成參數傳入中介者,以便中介者辨別是誰發生了改變。剩下的全部事情都交給中介者對象來完成,這樣一來,不管是修改仍是新增節點,都只須要改動中介者對象裏的代碼。
var goods = { "red|32G": 3, "red|16G": 0, "blue|32G":1, "blue|16G": 6 }; var mediator = (function () { var colorSelect = document.getElementById("colorSelect"), memorySelect = document.getElementById("memorySelect"), numberInput = document.getElementById("numberInput"), colorInfo = document.getElementById("colorInfo"), memoryInfo = document.getElementById("memoryInfo") numberInfo = document.getElementById("numberInfo"), nextBtn = document.getElementById("nextBtn"); return { changed: function (obj) { var color = colorSelect.value, memory = memorySelect.value, number = numberInput.value, stock = goods[color + '|' + memory]; if (obj === colorSelect) { colorInfo.innerHTML = color; } else if (obj === memorySelect){ memoryInfo.innerHTML = memory; } else if (obj === numberInput) { numberInfo.innerHTML = number; } if (!color) { nextBtn.disabled = true; nextBtn.innerHTML = '請選擇手機顏色'; return; } if (!memory) { nextBtn.disabled = true; nextBtn.innerHTML = '請選擇內存大小'; return; } if (((number - 0) | 0) !== number - 0) { nextBtn.disabled = true; nextBtn.innerHTML = '請輸入正確的購買數量'; return; } if (number > stock) { nextBtn.disabled = true; nextBtn.innerHTML = '庫存不足'; return; } nextBtn.disabled = false; nextBtn.innerHTML = '放入購物車'; } } })(); //事件函數 colorSelect.onchange = function () { mediator.changed(this); }; memorySelect.onchange = function () { mediator.changed(this); }; numberInput.oninput = function () { mediator.changed(this); };
能夠想象,某天咱們又要新增一些跟需求相關的節點,好比 CPU 型號,那咱們只須要稍稍改動 mediator 對象便可:
var goods = { //手機內存 "red|32G|800": 3, //顏色 red,內存 32G,cpu800,對應庫存數爲 3 "red|16G|801": 0, "blue|32G|800":1, "blue|16G|801": 6 }; var mediator = (function () { var colorInfo = document.getElementById("colorInfo"), cpuInfo = document.getElementById("cpuInfo"), //略 return { changed: function (obj) { var cpu = cpuSelect.value, stock = goods[color + '|' + memory + '|' + cpu]; //略 if (obj === cpuSelect) { cpuInfo.innerHTML = cpu; } //略 if (!cpu) { nextBtn.disabled = true; nextBtn.innerHTML = '請選擇 CPU 型號'; return; } //略 } } })(); cpuSelect.onchange = function () { mediator.changed(this); };
中介者模式是迎合迪米特法則的一種實現。迪米特法則也叫最少知識原則,是指一個對象應該儘量少地瞭解另外的對象。若是對象之間的耦合性過高,一個對象發生改變以後,不免會影響到其餘的對象,跟「城門失火殃及池魚」的道理是同樣的。而在中介者模式裏,對象之間幾乎不知道彼此的存在,它們只能經過中介者對象來互相瞭解對方。
所以,中介者模式使各個對象之間得以解耦,以中介者和對象之間的一對多關係取代了對象之間的網狀多對多關係。各個對象只須要關注自身功能的實現,對象之間的交互關係交給了中介者對象來實現和維護。
不過,中介者模式也存在一些缺點。其中,最大的缺點是系統中會新增一箇中介者對象,由於對象之間交互的複雜性,轉移成了中介者對象的複雜性,使得中介者對象常常是巨大的。中介者對象自身每每就是一個難以維護的對象。
中介者模式能夠很是方便地對模塊或者對象進行解耦,但對象之間並不是必定須要解耦。在實際項目中,模塊或對象之間有一些依賴關係是很正常的。畢竟咱們寫程序是爲了快速完成顯目交付生產,而不是堆砌模式和過渡設計。關鍵就在於如何去衡量對象之間的耦合程度。通常來講,若是對象之間的複雜耦合確實致使調用和維護出現了困難,並且這些耦合度隨項目的變化呈指數增加曲線,那咱們就能夠考慮用中介者模式來重構代碼。
參考書目:《JavaScript設計模式與開發實踐》