前面的話 程式由大大小小的單一對象組成,所有這些對象都按照某種關係和規則來通信。當程式的規模增大,對象會越來越多,它們之間的關係也越來越複雜,難免會形成網狀的交叉引用。當改變或刪除其中一個對象的時候,很可能需要通知所有引用到它的對象。面向對象設計鼓勵將行為分佈到各個對象中,把對象劃分成更小的粒度,有 ...
前面的話
程式由大大小小的單一對象組成,所有這些對象都按照某種關係和規則來通信。當程式的規模增大,對象會越來越多,它們之間的關係也越來越複雜,難免會形成網狀的交叉引用。當改變或刪除其中一個對象的時候,很可能需要通知所有引用到它的對象。面向對象設計鼓勵將行為分佈到各個對象中,把對象劃分成更小的粒度,有助於增強對象的可復用性,但由於這些細粒度對象之間的聯繫激增,又有可能會反過來降低它們的可復用性。中介者模式的作用就是解除對象與對象之間的緊耦合關係。增加一個中介者對象後,所有的相關對象都通過中介者對象來通信,而不是互相引用,所以當一個對象發生改變時,只需要通知中介者對象即可。中介者使各對象之間耦合鬆散,而且可以獨立地改變它們之間的交互。中介者模式使網狀的多對多關係變成了相對簡單的一對多關係。本文將詳細介紹中介者模式
泡泡堂
【初始版本】
先定義一個玩家構造函數,它有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個玩家對象:
//接下來創建2 個玩家對象: var player1 = new Player( 'xiaohuochai' ); var player2 = new Player( 'match' );
給玩家相互設置敵人:
//給玩家相互設置敵人: player1.enemy = player2; player2.enemy = player1;
當玩家player1被泡泡炸死的時候,只需要調用這一句代碼便完成了一局游戲:
player1.die();// 輸出:xiaohuochai lost、match won
只有2個玩家其實沒什麼意思,真正的泡泡堂游戲至多可以有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來給每個玩家設置隊友和敵人:
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(); } } };
最後定義一個工廠來創建玩家:
var playerFactory = function( 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( 'zhao', 'red' ), player2 = playerFactory( 'qian', 'red' ), player3 = playerFactory( 'sun', 'red' ), player4 = playerFactory( 'li', 'red' ); //藍隊: var player5 = playerFactory( 'zhou', 'blue' ), player6 = playerFactory( 'wu', 'blue' ), player7 = playerFactory( 'zheng', 'blue' ), player8 = playerFactory( 'wang', 'blue' );
現在已經可以隨意地為游戲增加玩家或者隊伍,但問題是,每個玩家和其他玩家都是緊緊耦合在一起的。在此段代碼中,每個玩家對象都有兩個屬性,this.partners和this.enemies,用來保存其他玩家對象的引用。當每個對象的狀態發生改變,比如角色移動、吃到道具或者死亡時,都必須要顯式地遍歷通知其他對象
如果在一個大型網路游戲中,畫面里有成百上千個玩家,幾十支隊伍在互相廝殺。如果有一個玩家掉線,必須從所有其他玩家的隊友列表和敵人列表中都移除這個玩家。游戲也許還有解除隊伍和添加到別的隊伍的功能,紅色玩家可以突然變成藍色玩家,這就不再僅僅是迴圈能夠解決的問題了
【中介者模式】
現在開始用中介者模式來改造上面的泡泡堂游戲,首先仍然是定義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 ); // 給中介者發送消息,玩家換隊 };
再繼續改寫之前創建玩家對象的工廠函數,可以看到,因為工廠函數里不再需要給創建的玩家對象設置隊友和敵人,這個工廠函數幾乎失去了工廠的意義:
var playerFactory=function(name,teamColor){ var newPlayer=newPlayer(name,teamColor); //創造一個新的玩家對象 playerDirector.reciveMessage('addPlayer',newPlayer); //給中介者發送消息,新增玩家 return newPlayer; };
最後,需要實現這個中介者playerDirector對象,一般有以下兩種方式
1、利用發佈—訂閱模式。將playerDirector實現為訂閱者,各player作為發佈者,一旦player的狀態發生改變,便推送消息給playerDirector,playerDirector處理消息後將反饋發送給其他player
2、在playerDirector中開放一些接收消息的介面,各player可以直接調用該介面來給playerDirector發送消息,player只需傳遞一個參數給playerDirector,這個參數的目的是使playerDirector可以識別發送者。同樣,playerDirector接收到消息之後會將處理結果反饋給其他player
這兩種方式的實現沒什麼本質上的區別。在這裡使用第二種方式,playerDirector開放一個對外暴露的介面reciveMessage,負責接收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(); // 本隊所有玩家lose } for ( var color in players ){ if ( color !== teamColor ){ var teamPlayers = players[ color ]; // 其他隊伍的玩家 for ( var i = 0, player; player = teamPlayers[ i++ ]; ){ player.win(); // 其他隊伍所有玩家win } } } } }; var reciveMessage = function(){ var message = Array.prototype.shift.call( arguments ); // arguments 的第一個參數為消息名稱 operations[ message ].apply( this, arguments ); }; return { reciveMessage: reciveMessage } })();
可以看到,除了中介者本身,沒有一個玩家知道其他任何玩家的存在,玩家與玩家之間的耦合關係已經完全解除,某個玩家的任何操作都不需要通知其他玩家,而只需要給中介者發送一個消息,中介者處理完消息之後會把處理結果反饋給其他的玩家對象。還可以繼續給中介者擴展更多功能,以適應游戲需求的不斷變化
購買商品
假設正在編寫一個手機購買的頁面,在購買流程中,可以選擇手機的顏色以及輸入購買數量,同時頁面中有兩個展示區域,分別向用戶展示剛剛選擇好的顏色和數量。還有一個按鈕動態顯示下一步的操作,需要查詢該顏色手機對應的庫存,如果庫存數量少於這次的購買數量,按鈕將被禁用並且顯示庫存不足,反之按鈕可以點擊並且顯示放入購物車
這個需求是非常容易實現的,假設已經提前從後臺獲取到了所有顏色手機的庫存量:
var goods ={ //手機庫存 "red":3, "blue":6 };
那麼頁面有可能顯示為如下幾種場景:
1、選擇紅色手機,購買4個,庫存不足
2、選擇藍色手機,購買5個,庫存充足,可以加入購物車
3、或者是沒有輸入購買數量的時候,按鈕將被禁用並顯示相應提示
接下來將遇到至少5個節點,分別是:
1、下拉選擇框colorSelect
2、文本輸入框numberInput
3、展示顏色信息colorInfo
4、展示購買數量信息numberInfo
5、決定下一步操作的按鈕nextBtn
【基礎版本】
從編寫HTML代碼開始
<body> 選擇顏色: <select id="colorSelect"> <option value="">請選擇</option> <option value="red">紅色</option> <option value="blue">藍色</option> </select> 輸入購買數量: <input type="text" id="numberInput"/> 您選擇了顏色: <div id="colorInfo"></div><br/> 您輸入了數量: <div id="numberInfo"></div><br/> <button id="nextBtn" disabled="true">請選擇手機顏色和購買數量</button> </body>
接下來將分別監聽colorSelect的onchange事件函數和numberInput的oninput事件函數,然後在這兩個事件中作出相應處理
<script> 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 ){ // 用戶輸入的購買數量是否為正整數 nextBtn.disabled = true; nextBtn.innerHTML = '請輸入正確的購買數量'; return; } if ( number > stock ){ // 當前選擇數量沒有超過庫存量 nextBtn.disabled = true; nextBtn.innerHTML = '庫存不足'; return ; } nextBtn.disabled = false; nextBtn.innerHTML = '放入購物車'; }; </script>
當觸發了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.onput裡面的代碼,因為在先前的代碼中,這些對象確實是耦合在一起的
當這個頁面里的節點激增到10個或者15個時,它們之間的聯繫可能變得更加錯綜複雜,任何一次改動都將變得很棘手。為了證實這一點,假設頁面中將新增另外一個下拉選擇框,代表選擇手機記憶體。現在需要計算顏色、記憶體和購買數量,來判斷nextBtn是顯示庫存不足還是放入購物車
首先要增加兩個HTML節點:
<body> 選擇顏色: <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/> 您選擇了顏色: <div id="colorInfo"></div><br/> 您選擇了記憶體: <div id="memoryInfo"></div><br/> 您輸入了數量: <div id="numberInfo"></div><br/> <button id="nextBtn" disabled="true">請選擇手機顏色和購買數量</button> </body> <script> var colorSelect = document.getElementById( 'colorSelect' ), numberInput = document.getElementById( 'numberInput' ), memorySelect = document.getElementById( 'memorySelect' ), colorInfo = document.getElementById( 'colorInfo' ), numberInfo = document.getElementById( 'numberInfo' ), memoryInfo = document.getElementById( 'memoryInfo' ), nextBtn = document.getElementById( 'nextBtn' ); </script>
接下來修改表示存庫的JSON對象以及修改colorSelect的onchange事件函數:
<script> var goods = { // 手機庫存 "red|32G": 3, // 紅色 32G,庫存數量為 3 "red|16G": 0, "blue|32G": 1, "blue|16G": 6 }; colorSelect.onchange = function(){ var color = this.value, memory = memorySelect.value, stock = goods[ color + '|' + memory ]; number = numberInput.value, // 數量 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 = '放入購物車'; }; </script>
當然同樣要改寫numberInput的事件相關代碼,具體代碼的改變跟colorSelect大同小異。最後還要新增memorySelect的onchange事件函數:
<script> memorySelect.onchange = function(){ var color = colorSelect.value, // 顏色 number = numberInput.value, // 數量 memory = this.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 = '放入購物車'; }; </script>
僅僅是增加一個記憶體的選擇條件,就要改變如此多的代碼,這是因為在目前的實現中,每個節點對象都是耦合在一起的,改變或者增加任何一個節點對象,都要通知到與其相關的對象
【引入中介者】
現在來引入中介者對象,所有的節點對象只跟中介者通信。當下拉選擇框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; } 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 cpuSelect = document.getElementById( 'cpuSelect' ); return { change: function(obj){ // 略 var cpu = cpuSelect.value, stock = goods[ color + '|' + memory + '|' + cpu ]; if ( obj === cpuSelect ){ cpuInfo.innerHTML = cpu; } } } })();
總結
中介者模式是迎合迪米特法則的一種實現。迪米特法則也叫最少知識原則,是指一個對象應該儘可能少地瞭解另外的對象。如果對象之間的耦合性太高,一個對象發生改變之後,難免會影響到其他的對象。而在中介者模式里,對象之間幾乎不知道彼此的存在,它們只能通過中介者對象來互相影響對方。因此,中介者模式使各個對象之間得以解耦,以中介者和對象之間的一對多關係取代了對象之間的網狀多對多關係。各個對象只需關註自身功能的實現,對象之間的交互關係交給了中介者對象來實現和維護
不過,中介者模式也存在一些缺點。其中,最大的缺點是系統中會新增一個中介者對象,因為對象之間交互的複雜性,轉移成了中介者對象的複雜性,使得中介者對象經常是巨大的。中介者對象自身往往就是一個難以維護的對象
中介者模式可以非常方便地對模塊或者對象進行解耦,但對象之間並非一定需要解耦。在實際項目中,模塊或對象之間有一些依賴關係是很正常的。畢竟寫程式是為了快速完成項目交付生產,而不是堆砌模式和過度設計。關鍵就在於如何去衡量對象之間的耦合程度。一般來說,如果對象之間的複雜耦合確實導致調用和維護出現了困難,而且這些耦合度隨項目的變化呈指數增長曲線,那就可以考慮用中介者模式來重構代碼