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ì)血管一般,即使一點很小的修改也必須小心翼翼,如圖14-1所示。

image

面向?qū)ο笤O(shè)計鼓勵將行為分布到各個對象中,把對象劃分成更小的粒度,有助于增強對象的可復(fù)用性,但由于這些細(xì)粒度對象之間的聯(lián)系激增,又有可能會反過來降低它們的可復(fù)用性。

中介者模式的作用就是解除對象與對象之間的緊耦合關(guān)系。增加一個中介者對象后,所有的相關(guān)對象都通過中介者對象來通信,而不是互相引用,所以當(dāng)一個對象發(fā)生改變時,只需要通知中介者對象即可。中介者使各對象之間耦合松散,而且可以獨立地改變它們之間的交互。中介者模式使網(wǎng)狀的多對多關(guān)系變成了相對簡單的一對多關(guān)系,如圖14-2所示。

image

在圖14-1中,如果對象A發(fā)生了改變,則需要同時通知跟A發(fā)生引用關(guān)系的B、D、E、F這4個對象;而在圖14-2中,使用中介者模式改進(jìn)之后,A發(fā)生改變時則只需要通知這個中介者對象即可。

現(xiàn)實中的中介者

在現(xiàn)實生活中也有很多中介者的例子,下面列舉幾個。

1.機場指揮塔

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

2.博彩公司

打麻將的人經(jīng)常遇到這樣的問題,打了幾局之后開始計算錢,A自摸了兩把,B杠了三次,C點炮一次給D,誰應(yīng)該給誰多少錢已經(jīng)很難計算清楚,而這還是在只有4個人參與的情況下。

在世界杯期間購買足球彩票,如果沒有博彩公司作為中介,上千萬的人一起計算賠率和輸贏絕對是不可能實現(xiàn)的事情。有了博彩公司作為中介,每個人只需和博彩公司發(fā)生關(guān)聯(lián),博彩公司會根據(jù)所有人的投注情況計算好賠率,彩民們贏了錢就從博彩公司拿,輸了錢就交給博彩公司。

中介者模式的例子——泡泡堂游戲

在游戲之初只支持兩個玩家同時進(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個玩家對象:

var player1 = new Player( '皮蛋' );
var player2 = new Player( '小乖' );

給玩家相互設(shè)置敵人:

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

當(dāng)玩家player1被泡泡炸死的時候,只需要調(diào)用這一句代碼便完成了一局游戲:

player1.die();// 輸出:皮蛋 lost、小乖 won

但不久過后就覺得只有2個玩家其實沒什么意思,真正的泡泡堂游戲至多可以有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è)置隊友和敵人:

var 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(){   // 玩家死亡

    var all_dead = true;
    this.state = 'dead'; // 設(shè)置玩家狀態(tài)為死亡

    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();
        }
    }
};

最后定義一個工廠來創(chuàng)建玩家:

var playerFactory = function( name, teamColor ){
    var newPlayer = new Player( name, teamColor );   // 創(chuàng)建新玩家

    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;
};

現(xiàn)在來感受一下, 用這段代碼創(chuàng)建8個玩家:

//紅隊:
   var player1 = playerFactory( '皮蛋', 'red' ),
       player2 = playerFactory( '小乖', 'red' ),
       player3 = playerFactory( '寶寶', 'red' ),
       player4 = playerFactory( '小強', 'red' );

//藍(lán)隊:
   var player5 = playerFactory( '黑妞', 'blue' ),
       player6 = playerFactory( '蔥頭', 'blue' ),
       player7 = playerFactory( '胖墩', 'blue' ),
       player8 = playerFactory( '海盜', 'blue' );

讓紅隊玩家全部死亡:

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

程序執(zhí)行結(jié)果如圖14-3所示。

image

2.玩家增多帶來的困擾

現(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)入投降模式。

3.用中介者模式改造泡泡堂游戲

現(xiàn)在我們開始用中介者模式來改造上面的泡泡堂游戲, 改造后的玩家對象和中介者的關(guān)系如圖14-4所示。

image

首先仍然是定義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.ReceiveMessage( 'playerDead', this );   // 給中介者發(fā)送消息,玩家死亡
};

/*******************移除玩家*****************/

Player.prototype.remove = function(){
    playerDirector.ReceiveMessage( 'removePlayer', this );    // 給中介者發(fā)送消息,移除一個玩家
};

/*******************玩家換隊*****************/

Player.prototype.changeTeam = function( color ){
    playerDirector.ReceiveMessage( 'changeTeam', this, color );   // 給中介者發(fā)送消息,玩家換隊
};

再繼續(xù)改寫之前創(chuàng)建玩家對象的工廠函數(shù),可以看到,因為工廠函數(shù)里不再需要給創(chuàng)建的玩家對象設(shè)置隊友和敵人,這個工廠函數(shù)幾乎失去了工廠的意義:

var playerFactory = function( name, teamColor ){
    var newPlayer = new Player( name, teamColor );    // 創(chuàng)造一個新的玩家對象
    playerDirector.ReceiveMessage( '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開放一個對外暴露的接口ReceiveMessage,負(fù)責(zé)接收player對象發(fā)送的消息,而player對象發(fā)送消息的時候,總是把自身this作為參數(shù)發(fā)送給playerDirector,以便playerDirector識別消息來自于哪個玩家對象,代碼如下:

var playerDirector= ( function(){
    var players = {},   // 保存所有玩家
       operations = {};   // 中介者可以執(zhí)行的操作

    /****************新增一個玩家***************************/
    operations.addPlayer = function( player ){
        var teamColor = player.teamColor;   // 玩家的隊伍顏色
        players[ teamColor ] = players[ teamColor ] || [];   // 如果該顏色的玩家還沒有成立隊伍,則
                                                                新成立一個隊伍

        players[ teamColor ].push( player );   // 添加玩家進(jìn)隊伍
    };

    /****************移除一個玩家***************************/
    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 ReceiveMessage = function(){
        var message = Array.prototype.shift.call( arguments );   // arguments的第一個參數(shù)為消息名稱
        operations[ message ].apply( this, arguments );
    };

    return {
        ReceiveMessage: ReceiveMessage
    }

})();

可以看到,除了中介者本身,沒有一個玩家知道其他任何玩家的存在,玩家與玩家之間的耦合關(guān)系已經(jīng)完全解除,某個玩家的任何操作都不需要通知其他玩家,而只需要給中介者發(fā)送一個消息,中介者處理完消息之后會把處理結(jié)果反饋給其他的玩家對象。我們還可以繼續(xù)給中介者擴展更多功能,以適應(yīng)游戲需求的不斷變化。

我們來看下測試結(jié)果:

// 紅隊:
var player1 = playerFactory( '皮蛋', 'red' ),
    player2 = playerFactory( '小乖', 'red' ),
    player3 = playerFactory( '寶寶', 'red' ),
    player4 = playerFactory( '小強', 'red' );

// 藍(lán)隊:
var player5 = playerFactory( '黑妞', 'blue' ),
    player6 = playerFactory( '蔥頭', 'blue' ),
    player7 = playerFactory( '胖墩', 'blue' ),
    player8 = playerFactory( '海盜', 'blue' );

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

運行結(jié)果如圖14-5所示。

image

假設(shè)皮蛋和小乖掉線,則結(jié)果如圖14-6所示。

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

image

假設(shè)皮蛋從紅隊叛變到藍(lán)隊,則結(jié)果如圖 14-7所示。

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

中介者模式的例子——購買商品

假設(shè)我們正在編寫一個手機購買的頁面,在購買流程中,可以選擇手機的顏色以及輸入購買數(shù)量,同時頁面中有兩個展示區(qū)域,分別向用戶展示剛剛選擇好的顏色和數(shù)量。還有一個按鈕動態(tài)顯示下一步的操作,我們需要查詢該顏色手機對應(yīng)的庫存,如果庫存數(shù)量少于這次的購買數(shù)量,按鈕將被禁用并且顯示庫存不足,反之按鈕可以點擊并且顯示放入購物車。

這個需求是非常容易實現(xiàn)的,假設(shè)我們已經(jīng)提前從后臺獲取到了所有顏色手機的庫存量:

var goods = {   // 手機庫存
    "red": 3,
    "blue": 6
};

那么頁面有可能顯示為如下幾種場景:

選擇紅色手機,購買4個,庫存不足。如圖14-8所示。

image

選擇藍(lán)色手機,購買5個,庫存充足,可以加入購物車。如圖14-9所示。

image

或者是沒有輸入購買數(shù)量的時候,按鈕將被禁用并顯示相應(yīng)提示。如圖14-10所示。

image

我們大概已經(jīng)能夠猜到,接下來將遇到至少5個節(jié)點,分別是:

  • 下拉選擇框 colorSelect

  • 文本輸入框 numberInput

  • 展示顏色信息 colorInfo

  • 展示購買數(shù)量信息 numberInfo

  • 決定下一步操作的按鈕 nextBtn

1.開始編寫代碼

我們從編寫HTML代碼開始。

<body>
        選擇顏色:  <select id="colorSelect">
                      <option value="">請選擇</option>
                      <option value="red">紅色</option>
                      <option value="blue">藍(lán)色</option>
                   </select>


        輸入購買數(shù)量:  <input type="text" id="numberInput"/>


        您選擇了顏色:  <div id="colorInfo"></div><br/>
        您輸入了數(shù)量:  <div id="numberInfo"></div><br/>

        <button id="nextBtn" disabled="true">請選擇手機顏色和購買數(shù)量</button>
</body>

接下來將分別監(jiān)聽colorSelect的onchange事件函數(shù)和numberInput的oninput事件函數(shù),然后在這兩個事件中作出相應(yīng)處理。

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,   // 數(shù)量
            stock = goods[ color ];   // 該顏色手機對應(yīng)的當(dāng)前庫存

        colorInfo.innerHTML = color;

        if ( !color ){
            nextBtn.disabled = true;
            nextBtn.innerHTML = '請選擇手機顏色';
            return;
        }

        if ( ( ( number - 0 ) | 0 ) !== number - 0 ){   // 用戶輸入的購買數(shù)量是否為正整數(shù)
            nextBtn.disabled = true;
            nextBtn.innerHTML = '請輸入正確的購買數(shù)量';
            return;
        }

        if ( number > stock  ){   // 當(dāng)前選擇數(shù)量超過庫存量
            nextBtn.disabled = true;
            nextBtn.innerHTML = '庫存不足';
            return ;
        }

        nextBtn.disabled = false;
        nextBtn.innerHTML = '放入購物車';

    };

2.對象之間的聯(lián)系

來考慮一下,當(dāng)觸發(fā)了colorSelect的onchange之后,會發(fā)生什么事情。

首先我們要讓colorInfo中顯示當(dāng)前選中的顏色,然后獲取用戶當(dāng)前輸入的購買數(shù)量,對用戶的輸入值進(jìn)行一些合法性判斷。再根據(jù)庫存數(shù)量來判斷nextBtn的顯示狀態(tài)。

別忘了,我們還要編寫numberInput的事件相關(guān)代碼:

numberInput.oninput = function(){
    var color = colorSelect.value,   // 顏色
        number = this.value,   // 數(shù)量
        stock = goods[ color ];   // 該顏色手機對應(yīng)的當(dāng)前庫存

    numberInfo.innerHTML = number;

    if ( !color ){
        nextBtn.disabled = true;
        nextBtn.innerHTML = '請選擇手機顏色';
        return;
    }

    if ( ( ( number - 0 ) | 0 ) !== number - 0 ){   // 輸入購買數(shù)量是否為正整數(shù)
        nextBtn.disabled = true;
        nextBtn.innerHTML = '請輸入正確的購買數(shù)量';
        return;
    }

    if ( number > stock  ){   // 當(dāng)前選擇數(shù)量沒有超過庫存量
        nextBtn.disabled = true;
        nextBtn.innerHTML = '庫存不足';
        return ;
    }

    nextBtn.disabled = false;
    nextBtn.innerHTML = '放入購物車';

};

2.可能遇到的困難

雖然目前順利完成了代碼編寫,但隨之而來的需求改變有可能給我們帶來麻煩。假設(shè)現(xiàn)在要求去掉colorInfo和numberInfo這兩個展示區(qū)域,我們就要分別改動colorSelect.onchange和numberInput.oninput里面的代碼,因為在先前的代碼中,這些對象確實是耦合在一起的。

目前我們面臨的對象還不算太多,當(dāng)這個頁面里的節(jié)點激增到10個或者15個時,它們之間的聯(lián)系可能變得更加錯綜復(fù)雜,任何一次改動都將變得很棘手。為了證實這一點,我們假設(shè)頁面中將新增另外一個下拉選擇框,代表選擇手機內(nèi)存。現(xiàn)在我們需要計算顏色、內(nèi)存和購買數(shù)量,來判斷nextBtn是顯示庫存不足還是放入購物車。

首先我們要增加兩個HTML節(jié)點:

<body>

    選擇顏色:    <select id="colorSelect">
                    <option value="">請選擇</option>
                    <option value="red">紅色</option>
                    <option value="blue">藍(lán)色</option>
                </select>

    選擇內(nèi)存:    <select id="memorySelect">
                    <option value="">請選擇</option>
                    <option value="32G">32G</option>
                    <option value="16G">16G</option>
                </select>

    輸入購買數(shù)量: <input type="text" id="numberInput"/><br/>

    您選擇了顏色: <div id="colorInfo"></div><br/>
    您選擇了內(nèi)存: <div id="memoryInfo"></div><br/>
    您輸入了數(shù)量: <div id="numberInfo"></div><br/>

    <button id="nextBtn" disabled="true">請選擇手機顏色和購買數(shù)量</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事件函數(shù):

<script>
    var goods = {   // 手機庫存
        "red|32G": 3,   // 紅色32G,庫存數(shù)量為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,   // 數(shù)量
        colorInfo.innerHTML = color;

        if ( !color ){
            nextBtn.disabled = true;
            nextBtn.innerHTML = '請選擇手機顏色';
            return;
        }
        if ( !memory ){
            nextBtn.disabled = true;
            nextBtn.innerHTML = '請選擇內(nèi)存大小';
            return;
        }
        if ( ( ( number - 0 ) | 0 ) !== number - 0 ){   // 輸入購買數(shù)量是否為正整數(shù)
            nextBtn.disabled = true;
            nextBtn.innerHTML = '請輸入正確的購買數(shù)量';
            return;
        }
        if ( number > stock  ){   // 當(dāng)前選擇數(shù)量沒有超過庫存量
            nextBtn.disabled = true;
            nextBtn.innerHTML = '庫存不足';
            return ;
        }
        nextBtn.disabled = false;
        nextBtn.innerHTML = '放入購物車';
    };
</script>

當(dāng)然我們同樣要改寫numberInput的事件相關(guān)代碼,具體代碼的改變跟colorSelect大同小異,讀者可以自行實現(xiàn)。

最后還要新增memorySelect的onchange事件函數(shù):

<script>
    memorySelect.onchange = function(){

        var color = colorSelect.value,   // 顏色
            number = numberInput.value,   // 數(shù)量
            memory = this.value,
            stock = goods[ color + '|' + memory ];   // 該顏色手機對應(yīng)的當(dāng)前庫存
            memoryInfo.innerHTML = memory;

        if ( !color ){
            nextBtn.disabled = true;
            nextBtn.innerHTML = '請選擇手機顏色';
            return;
        }
        if ( !memory ){
            nextBtn.disabled = true;
            nextBtn.innerHTML = '請選擇內(nèi)存大小';
            return;
        }
        if ( ( ( number - 0 ) | 0 ) !== number - 0 ){   // 輸入購買數(shù)量是否為正整數(shù)
            nextBtn.disabled = true;
            nextBtn.innerHTML = '請輸入正確的購買數(shù)量';
            return;
        }
        if ( number > stock ){   // 當(dāng)前選擇數(shù)量沒有超過庫存量
            nextBtn.disabled = true;
            nextBtn.innerHTML = '庫存不足';
            return ;
        }

        nextBtn.disabled = false;
        nextBtn.innerHTML = '放入購物車';

    };
</script>

很遺憾,我們僅僅是增加一個內(nèi)存的選擇條件,就要改變?nèi)绱硕嗟拇a,這是因為在目前的實現(xiàn)中,每個節(jié)點對象都是耦合在一起的,改變或者增加任何一個節(jié)點對象,都要通知到與其相關(guān)的對象。

4.引入中介者

現(xiàn)在我們來引入中介者對象,所有的節(jié)點對象只跟中介者通信。當(dāng)下拉選擇框colorSelect、memorySelect和文本輸入框numberInput發(fā)生了事件行為時,它們僅僅通知中介者它們被改變了,同時把自身當(dāng)作參數(shù)傳入中介者,以便中介者辨別是誰發(fā)生了改變。剩下的所有事情都交給中介者對象來完成,這樣一來,無論是修改還是新增節(jié)點,都只需要改動中介者對象里的代碼。

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,// 內(nèi)存
                number = numberInput.value,   // 數(shù)量
                stock = goods[ color + '|' + memory ];   // 顏色和內(nèi)存對應(yīng)的手機庫存數(shù)量

            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 = '請選擇內(nèi)存大小';
                return;
            }

            if ( ( ( number - 0 ) | 0 ) !== number - 0 ){   // 輸入購買數(shù)量是否為正整數(shù)
                nextBtn.disabled = true;
                nextBtn.innerHTML = '請輸入正確的購買數(shù)量';
                return;
            }

            nextBtn.disabled = false;
            nextBtn.innerHTML = '放入購物車';

        }
    }

})();

// 事件函數(shù):
colorSelect.onchange = function(){
    mediator.changed( this );
};
memorySelect.onchange = function(){
    mediator.changed( this );
};
numberInput.oninput = function(){
    mediator.changed( this );
};

可以想象,某天我們又要新增一些跟需求相關(guān)的節(jié)點,比如CPU型號,那我們只需要稍稍改動mediator對象即可:

var goods = {    // 手機庫存
    "red|32G|800": 3,    // 顏色red,內(nèi)存32G,cpu800,對應(yīng)庫存數(shù)量為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;
            }
                   // 略
        }
    }
})();

小結(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ù)的對象。

我們都知道,毒販子雖然使吸毒者和制毒者之間的耦合度降低,但毒販子也要抽走一部分利潤。同樣,在程序中,中介者對象要占去一部分內(nèi)存。而且毒販本身還要防止被警察抓住,因為它了解整個犯罪鏈條中的所有關(guān)系,這表明中介者對象自身往往是一個難以維護(hù)的對象。

中介者模式可以非常方便地對模塊或者對象進(jìn)行解耦,但對象之間并非一定需要解耦。在實際項目中,模塊或?qū)ο笾g有一些依賴關(guān)系是很正常的。畢竟我們寫程序是為了快速完成項目交付生產(chǎn),而不是堆砌模式和過度設(shè)計。關(guān)鍵就在于如何去衡量對象之間的耦合程度。一般來說,如果對象之間的復(fù)雜耦合確實導(dǎo)致調(diào)用和維護(hù)出現(xiàn)了困難,而且這些耦合度隨項目的變化呈指數(shù)增長曲線,那我們就可以考慮用中介者模式來重構(gòu)代碼。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • javascript設(shè)計模式與開發(fā)實踐 設(shè)計模式 每個設(shè)計模式我們需要從三點問題入手: 定義 作用 用法與實現(xiàn) 單...
    穿牛仔褲的蚊子閱讀 4,489評論 0 13
  • 工廠模式 單體模式 模塊模式 代理模式 職責(zé)鏈模式 命令模式 模板方法模式 策略模式 發(fā)布-訂閱模式 中介者模式 ...
    HelloJames閱讀 1,080評論 0 6
  • 工廠模式類似于現(xiàn)實生活中的工廠可以產(chǎn)生大量相似的商品,去做同樣的事情,實現(xiàn)同樣的效果;這時候需要使用工廠模式。簡單...
    舟漁行舟閱讀 8,131評論 2 17
  • 概念 ??中介者模式的作用解除對象與對象之間的緊耦合關(guān)系,增加一個中介者對象后,所有的相關(guān)對象都通過中介者對象來通...
    小小的開發(fā)人員閱讀 543評論 0 0
  • 單例模式 適用場景:可能會在場景中使用到對象,但只有一個實例,加載時并不主動創(chuàng)建,需要時才創(chuàng)建 最常見的單例模式,...
    Obeing閱讀 2,315評論 1 10

友情鏈接更多精彩內(nèi)容