1. 程式人生 > 實用技巧 >《JavaScript 模式》讀書筆記(7)— 設計模式3

《JavaScript 模式》讀書筆記(7)— 設計模式3

  這一篇,我們學習本篇中最為複雜的三個設計模式,代理模式、中介者模式以及觀察者模式。這三個模式很重要!!

七、代理模式

  在代理設計模式中,一個物件充當另一個物件的介面。它與外觀模式的區別之處在於,外觀模式中您所擁有的是合併了多個方法呼叫的便利方法。代理則介於物件的客戶端和物件本身之間,並且對該物件的訪問進行保護。

  這種模式可能看起來像是額外的開銷,但是處於效能因素的考慮它卻非常有用。代理充當了某個物件(也稱為“本體物件”)的守護物件,並且試圖使本體物件做盡可能少的工作。

  使用這種模式的其中一個例子是我們可以稱為延遲初始化(lazy initialization)的方法。試想一下,假設初始化本體物件的開銷非常大,而恰好又在客戶端初始化該本體物件以後,應用程式實際上卻從來沒有使用過它。在這種情況下,代理可以通過替換本體物件的介面來解決這個問題。代理接收初始化請求,但是直到該本體物件明確的將被實際使用之前,代理從不會將該請求傳遞給本體物件。

  下圖舉例說明了這種情況,即首先由客戶端發出一個初始化請求,然後代理以一切正常作為響應,但實際上卻並沒有將該訊息傳遞到本體物件,直到客戶端明顯需要本體物件完成一些工作的時候。只有那個時候,代理才將兩個訊息一起傳遞。

範例

  當本體物件執行一些開銷很大的操作時,代理模式就顯得非常有用。在Web應用中,可以執行的開銷最大的操作就是網路請求,因此,儘可能合併更多的HTTP請求就顯得非常重要。讓我們舉一個例子,該例子執行了上述操作並演示代理模式所起的作用。

視訊展開

  假定我們有一個可以播放選定藝術家視訊的小應用程式。實際上,您可以將該線上示例用作娛樂,並且還可以在網址http://www.jspatterns.com/book/7/proxy.html

檢視其程式碼。

  在該頁面有一個視訊標題的清單。當用戶點選一個視訊標題時,該標題下面的區域將張開顯示有關視訊的更多資訊,並且還能啟用視訊播放功能。詳細的視訊資訊和網址並不是該頁面的一部分,這需要通過建立Web服務呼叫以進行檢索才能獲得這些資訊,Web服務可以接受以多個視訊ID作為引數的查詢,因此我們可以通過構造更少的HTTP請求數量並且每次檢索多個視訊的資料,從而加速該應用程式。

  我們的應用程式支援同時點開多個(或全部)視訊資訊,因此這是合併Web服務請求的一個完美機會。

沒有代理的情況

  在該應用程式中,主要的“參與者”有兩個物件:

  Videos:負責展開/摺疊[方法videos.getInfo()]資訊區域以及播放視訊(方法videos.getPlayer)。

  http:通過呼叫方法http.makeRequest()來負責與伺服器的通訊。

  當沒有代理的時候,videos.getInfo()將針對每個視訊呼叫一次http.makeRequest()。當我們新增一個代理時,它將成為一個新的稱之為proxy的參與者,它位於物件的videos和物件http之間,並且將呼叫委託給makeRequest(),此外還在可能的情況下合併這些呼叫。

  下面,讓我們首先來看看在沒有代理的情況下的程式碼,然後再新增代理以提高應用程式的響應能力。

HTML

  下面的HTML程式碼只是一個連結列表:

<p><span id="toggle-all">Toggle Checked</span></p>
<ol id="vids">
    <li><input type="checkbox" checked><a 
        href="http://new.music.yahoo.com/videos/--2158073">Gravedigger</a></li>
    <li><input type="checkbox" checked><a 
        href="http://new.music.yahoo.com/videos/--4472739">Save Me</a></li>    
    <li><input type="checkbox" checked><a 
        href="http://new.music.yahoo.com/videos/--45286339">Crush</a></li>
    <li><input type="checkbox" checked><a 
        href="http://new.music.yahoo.com/videos/--2144530">Don't Drink The Water</a></li>
    <li><input type="checkbox" checked><a 
        href="http://new.music.yahoo.com/videos/--217241800">Funny the Way It Is</a></li>    
    <li><input type="checkbox" checked><a 
        href="http://new.music.yahoo.com/videos/--2144532">What Would You Say</a></li>    
</ol>

 

事件處理程式

  現在讓我們開檢視該事件處理程式。首先定義下面便利的簡寫函式$:

var $ = function (id) {
    return document.getElementById(id);
};

  下面程式碼中使用了事件委託(event delegation, 有關此模式資訊請參見第8章)模式,讓我們使用單個函式來處理在id為“vids”(即id='vids')的有序列表中出現的所有點選。

$('vids').onclick = function (e) {
    var src, id;
    
    e = e || window.event;
    src = e.target || e.srcElement;
    
    if (src.nodeName.toUpperCase() !== "A") {
        return;
    }
    
    if (typeof e.preventDefault === "function") {
        e.preventDefault();
    }
    e.returnValue = false;
    
    id = src.href.split('--')[1];
    
    if (src.className === "play") {
        src.parentNode.innerHTML = videos.getPlayer(id);
        return;
    } 
    
    src.parentNode.id = "v" + id;
    videos.getInfo(id);    
};

  在以上包羅永珍的點選處理程式(click handler)中,我們對其中的兩次點選非常感興趣:一個是展開/摺疊資訊部分【呼叫getInfo()】;另一個是播放視訊(當目標的類名為“play”時),這意味著資訊區域已被展開,然後我們便可以呼叫getPlayer()。其中,這些影片的ID提取自連結hrefs。

  另外一個點選處理程式對點選作出反應時將切換所有的資訊片段(info section),它本質上只是再次呼叫了getInfo(),不過是在一個迴圈中呼叫getInfo():

$('toggle-all').onclick = function (e) {

    var hrefs,
        i, 
        max,
        id;
    
    hrefs = $('vids').getElementsByTagName('a');
    for (i = 0, max = hrefs.length; i < max; i += 1) {
        // skip play links
        if (hrefs[i].className === "play") {
            continue;
        }
        // skip unchecked
        if (!hrefs[i].parentNode.firstChild.checked) {
            continue;
        }
        
        id = hrefs[i].href.split('--')[1];
        hrefs[i].parentNode.id = "v" + id;
        videos.getInfo(id);
    }
};

viedos物件

  該videos物件由三個方法:

  getPlayer():返回HTML請求以播放Flash視訊。

  updateList():該回調函式接收所有來自Web服務的資料,並且生成HTML程式碼以用於擴充套件資訊片段中。在這個方法中根本沒有什麼特別有趣的事情發生。

  getInfo():該方法用於切換資訊片段的可見性,並且還在http物件的呼叫中將updateList()作為回撥函式傳遞出去。

  下面是該物件的一個程式碼片段:

var videos = {
    getPlayer: function (id) {...},
    
    updateList: function (data) {...},

    getInfo: function (id) {
        
        var info = $('info' + id);
        
        if (!info) {
            proxy.makeRequest(id, videos.updateList, videos);
            return;
        }
        
        if (info.style.display === "none") {
            info.style.display = '';
        } else {
            info.style.display = 'none';
        }   
    }
};

 

http物件

  http 物件只有一個方法,該方法產生JSONP格式的請求以傳送到Yahoo!的YQL Web服務:

var http = {
    makeRequest: function (ids, callback) {
        var url = 'http://query.yahooapis.com/v1/public/yql?q=',
            sql = 'select * from music.video.id where ids IN ("%ID%")',
            format = "format=json",
            handler = "callback=" + callback,
            script = document.createElement('script');
        
        sql = sql.replace('%ID%', ids.join('","'));
        sql = encodeURIComponent(sql);
        
        url += sql + '&' + format + '&' + handler;
        script.src = url;
        
        document.body.appendChild(script);   
    }
};

  注意:YQL(雅虎查詢語言)是一種元(meta)Web服務,它提供了一種通過使用類似SQL的語法以獲取大量其他Web服務的能力,且無需研究每個服務的API。

  當所有六個視訊同時切換時,六個獨立的請求將被髮送到Web服務,YQL查詢的語法如下所示:

select * from music.video.id where ids IN ("2158073")

進入代理模式

  前面介紹的程式碼執行良好,但是我們可以更進一步。現在讓proxy物件進入本場景並且接管HTTP和videos之間的通訊。它試圖使用一個簡單的邏輯將多個請求合併起來:即一個50ms的視訊緩衝區。videos物件並不直接呼叫HTTP服務而是呼叫proxy。然後,該proxy在轉發該請求之前一直等待。如果來自於videos的其他呼叫進入了50ms的等待期,這些請求將會被合併為單個請求。50ms的延遲對於使用者而言是相當不易察覺的,但是卻有助於合併請求,此外,當點選“切換(toggle)”並同時展開超過一個的視訊時,該延遲還能加速使用者體驗。此外,它還顯著降低了伺服器的負載,這是由於該Web伺服器僅需要處理數量更少的請求。

  將兩個視訊請求合併以後的YQL查詢將如下所示:

select * from music.video.id where ids IN ("2158073", "123456")

  在修改版本的程式碼中,其唯一的變化在於videos.getInfo()現在呼叫的是proxy.makeRequest(),而不是http.makeRequest(),如下所示:

proxy.makeRequest(id, videos.updateList, videos);

  該proxy建立了一個佇列以收集過去50ms接收到的視訊ID,然後排空該佇列,同時還呼叫http並向它提供自己的回撥函式,這是由於videos.updateList()回撥函式僅能處理單個數據記錄。

  下面是該proxy的程式碼:

var proxy = {
    ids: [],
    delay: 50,
    timeout: null,
    callback: null,
    context: null,
    makeRequest: function (id, callback, context) {
        
        // add to the queue
        this.ids.push(id);
        
        this.callback = callback;
        this.context  = context;
        
        // set up timeout
        if (!this.timeout) {
            this.timeout = setTimeout(function () {
                proxy.flush();
            }, this.delay);
        }
    },
    flush: function () {
        
        http.makeRequest(this.ids, "proxy.handler");
                
        // clear timeout and queue
        this.timeout = null;
        this.ids = [];
        
    },
    handler: function (data) {        
        var i, max;
        
        // single video
        if (parseInt(data.query.count, 10) === 1) {
            proxy.callback.call(proxy.context, data.query.results.Video);
            return;
        }
        
        // multiple videos
        for (i = 0, max = data.query.results.Video.length; i < max; i += 1) {
            proxy.callback.call(proxy.context, data.query.results.Video[i]);
        } 
    }
};

  通過引入該代理,僅需對原始程式碼進行簡單的修改就能夠提供將多個Web服務請求合併成單個請求的能力。

  下圖分別舉例說明了生成三輪往返訊息到服務(無代理時)與使用代理時僅有一輪往返訊息相比較的情景。

快取代理

  在本例子中,客戶端物件(videos)足夠聰明到不會再次請求同一個視訊的訊息。但是實際情況並不總是如此。代理可以通過將以前的請求結果快取到新的cache屬性中(見下圖),從而更進一步的保護對本體物件http的訪問。那麼,如果videos物件恰好再一次請求有關同一個視訊ID的資訊,proxy可以從快取中取出該資訊,從而節省了該網路往返訊息。

  最後,該模式的完整程式碼,可以從開始附上的連結地址找到。

八、中介者模式

  應用程式,無論其大小,都是由一些單個物件所組成。所有這些物件需要一種方式來實現相互通訊,而這種通訊方式在一定程度上不降低可維護性,也不損害那種安全的改變部分應用程式而不會破壞其餘部分的能力。隨著應用程式的增長,將新增越來越多的物件。然後再程式碼重構期間,物件將被刪除或重新整理。當物件互相知道太多資訊並且直接通訊(呼叫對方的方法並改變屬性)時,這將會導致產生不良的緊耦合(tight coupling)問題。當物件間緊密耦合時,很難在改變單個物件的同時不影響其他多個物件。因而,即使對應用程式進行最簡單的修改也變得不再容易,而且幾乎無法估計修改可能花費的時間。

  中介者模式緩解了該問題並促進形成鬆耦合(loose coupling),而且還有助於提高可維護性(見下圖)。在這種模式中,獨立的物件(下圖中的colleague)之間並不直接通訊,而是通過mediator物件。當其中一個colleague物件改變狀態以後,它將會通知該mediator,而mediator將會把該變化傳達到任意其他應該知道此變化的colleague物件。

中介者示例

  下面讓我們探討使用中介模式的例子。該應用程式是一個遊戲程式,其中兩名玩家分別給予半分鐘的時間以競爭決勝出誰會比另一個按更多次數的按鈕。在比賽中玩家1按2,而玩家2按0(這樣他們會更舒服一點,而不會為了爭奪鍵盤而爭吵)。記分板依據當前得分進行更新。

  本例子中參與的物件如下所示:

  • 玩家1。
  • 玩家2。
  • 記分板(Scoreboard)。
  • 中介者(Mediator)。

  中介者知道所有其他物件的資訊。他與輸入裝置(鍵盤)進行通訊並處理鍵盤按鍵事件,並且還要決定是那個玩家前進了一個回合,隨後還將該訊息通知給玩家(見下圖)。玩家玩遊戲的同時(即僅用一分來更新其自己的分數),還要通知中介者它所做的事情。中介者將更新後的分數傳達給記分板,記分板隨後更新現實的分值。

  除了中介者以外,沒有物件知道任何其他物件。這種模式使得更新遊戲變得非常簡單,比如,通過該中介者可以很容易新增一個新的玩家或者另一個顯示剩餘時間的顯示視窗。

  可以在網址http://www.jspatterns.com/book/7/mediator.html看到該遊戲的線上版本及原始碼。

  player物件是由Player()建構函式所建立的,具有points和name屬性。原型中的play()方法每次以1遞增分數,然後通知中介者。

// player constructor
function Player(name) {
    this.points = 0;
    this.name = name;
}
// play method
Player.prototype.play = function () {
    this.points += 1;
    mediator.played();
};

  scoreboard物件中有一個update()方法,在輪到每個玩家遊戲結束之後mediator物件將呼叫該方法。scoreboard並不知道任何玩家的介面並且也沒有儲存分值,它僅根據mediator給定的值顯示當前分數:

// the scoreboard object
var scoreboard = {
    
    // HTML element to be updated
    element: document.getElementById('results'),
    
    // update the score display
    update: function (score) {
        
        var i, msg = '';
        for (i in score) {
            if (score.hasOwnProperty(i)) {
                msg += '<p><strong>' + i + '<\/strong>: ';
                msg += score[i];
                msg += '<\/p>';
            }
        }
        this.element.innerHTML = msg;
    }
};

  現在,讓我們來檢視一下mediator物件。他首先初始化遊戲,在它的setup()方法中建立player物件,然後將這些player物件記錄到自己的players屬性中。其中,player()方法將在每輪遊戲後由player所呼叫。該方法更新score雜湊表並將其傳送到scoreboard中以用於顯示分值。最後一個方法為keypress(),它用於處理鍵盤時間,確定那個玩家前進了一個回合並通知該玩家。

var mediator = {
    
    // all the players
    players: {},
    
    // 
    setup: function () {
        var players = this.players;
        players.home = new Player('Home');
        players.guest = new Player('Guest');
        
    },
    
    // someone plays, update the score
    played: function () {
        var players = this.players,
            score = {
                Home:  players.home.points,
                Guest: players.guest.points
            };
            
        scoreboard.update(score);
    },
    
    // handle user interactions
    keypress: function (e) {
        e = e || window.event; // IE
        if (e.which === 49) { // key "1"
            mediator.players.home.play();
            return;
        }
        if (e.which === 48) { // key "0"
            mediator.players.guest.play();
            return;
        }
    }
};

  而最後的事情就是要建立以及拆除該遊戲:

// go!
mediator.setup();
window.onkeypress = mediator.keypress;

// game over in 30 seconds
setTimeout(function () {
    window.onkeypress = null;
    alert('Game over!');
}, 30000);

九、觀察者模式

  觀察者(observer)模式廣泛應用於客戶端JavaScript程式設計中。所有的瀏覽器事件(滑鼠懸停、按鍵等事件)是該模式的例子。它的另一個名字也稱為自定義事件(custom events),與那些由瀏覽器觸發的事件相比,自定義事件表示是由您程式設計實現的事件。此外,該模式的另外一個別名是訂閱/釋出(subscriber/publisher)模式。

  設計這種模式背後的主要動機是促進形成鬆散耦合。在這種模式中,並不是一個物件呼叫另一個物件的方法,而是一個物件訂閱另一個物件的特定活動並在狀態改變後獲得通知。訂閱者也稱之為觀察者,而被觀察的物件成為釋出者或者主題。當發生了一個重要的事件時,釋出者將會通知(呼叫)所有訂閱者並且可能經常以事件物件的形式傳遞訊息。

示例#1:雜誌訂閱

  為了理解如何實現這種模式,讓我們看一個具體的例子。假設有一個釋出者paper,他每天出版報紙以及月刊雜誌。訂閱者joe將被通知任何時候所發生的新聞。

  該paper物件需要有一個subscribers屬性,該屬性是一個儲存所有訂閱者的陣列。訂閱行為只是將其加入到這個陣列中。當一個事件發生時,paper會迴圈遍歷訂閱者列表並通知他們。通知意味著呼叫訂閱者物件的某個方法。因此,當用戶訂閱資訊的時候,該訂閱者需要向paper的subscribe()提供它的其中一個方法。

  paper也提供了unsubscribe()方法,該方法表示從訂閱者陣列(即subscribes屬性)中刪除訂閱者。Paper最後一個重要的方法是publish(),它會呼叫這些訂閱者的方法。總而言之,釋出者物件paper需要具有以下這些成員:

  subscribers:一個數組。

  subscribe():將訂閱者新增到subscribers陣列。

  unsubscribe():從訂閱者陣列subscribers中刪除訂閱者。

  publish():迴圈遍歷subscribers中的每個元素,並且呼叫他們註冊時所提供的方法。

  所有這三種方法都需要一個type引數,因為釋出者可能觸發多個事件(比如同時釋出一本雜誌和一份報紙)而使用者可能僅選擇訂閱其中一種,而不是另外一種。

  由於這些成員對於任何釋出者物件都是通用的,將它們作為獨立物件的一個部分來實現是很有意義的。那樣我們可以將其複製到任何物件中,並且將任意給定的物件變成一個釋出者。

  下面是該通用釋出者功能的一個實現示例,它定義了前面列嶼出的所有需要的成員,還加上了一個幫助方法visitSubscribers():

var publisher = {
    subscribers: {
        any: [] // event type: subscribers
    },
    subscribe: function (fn, type) {
        type = type || 'any';
        if (typeof this.subscribers[type] === "undefined") {
            this.subscribers[type] = [];
        }
        this.subscribers[type].push(fn);
    },
    unsubscribe: function (fn, type) {
        this.visitSubscribers('unsubscribe', fn, type);
    },
    publish: function (publication, type) {
        this.visitSubscribers('publish', publication, type);
    },
    visitSubscribers: function (action, arg, type) {
        var pubtype = type || 'any',
            subscribers = this.subscribers[pubtype],
            i,
            max = subscribers.length;
            
        for (i = 0; i < max; i += 1) {
            if (action === 'publish') {
                subscribers[i](arg);
            } else {
                if (subscribers[i] === arg) {
                    subscribers.splice(i, 1);
                }
            }
        }
    }
};

  而這裡有一個函式makePublisher(),它接受一個物件作為引數,通過把上述通用釋出者的方法複製到該物件中,從而將其轉換為一個釋出者:

function makePublisher(o) {
    var i;
    for (i in publisher) {
        if (publisher.hasOwnProperty(i) && typeof publisher[i] === "function") {
            o[i] = publisher[i];
        }
    }
    o.subscribers = {any: []};
}

  現在,讓我們來實現paper物件。它所能做的就是釋出日報和月刊:

var paper = {
    daily: function () {
        this.publish("big news today");
    },
    monthly: function () {
        this.publish("interesting analysis", "monthly");
    }
};

  將paper構造成一個釋出者:

makePublisher(paper);

  由於已經有了一個釋出者,讓我們來看看訂閱者物件joe,該物件有兩個方法:

var joe = {
    drinkCoffee: function (paper) {
        console.log('Just read ' + paper);
    },
    sundayPreNap: function (monthly) {
        console.log('About to fall asleep reading this ' + monthly);
    }
};

  現在,paper註冊joe(也就是說,joe向paper訂閱):

paper.subscribe(joe.drinkCoffee);
paper.subscribe(joe.sundayPreNap, 'monthly');

  正如您所看到的,joe為預設“任意”事件提供了一個可被呼叫的方法,而另一個可被呼叫的方法則用於當“monthly”型別的事件發生時的情況。現在,讓我們觸發一些事件:

paper.daily();
paper.daily();
paper.daily();
paper.monthly();

  所有這些出版物產生的事件將會呼叫joe的適當方法,控制檯中輸出的結果如下所示:

Just read big news today
observer.html:89 Just read big news today
observer.html:89 Just read big news today
observer.html:92 About to fall asleep reading this interesting analysis

  該程式碼好的部分在於,paper物件中並沒有硬編碼joe,而joe中也並沒有硬編碼paper。此外,本程式碼中還沒有那些知道所有一切的中介者物件。由於參與物件是鬆耦合的,我們可以向paper新增更多的訂閱者而根本不需要修改這些物件。

  讓我們將這個例子更進一步擴充套件並且讓joe稱為釋出者(畢竟,使用部落格和微博時任何人都可以是出版者)。因此,joe變成了一個釋出者並且可以在Twitter上分發狀態更新:

makePublisher(joe);

joe.tweet = function (msg) {
    this.publish(msg);
};

  現在想象一下,paper的公關部分決定讀取讀者的tweet,並且訂閱joe的資訊,那麼需要提供方法readTweets():

paper.readTweets = function (tweet) {
    alert('Call big meeting! Someone ' + tweet);
};

joe.subscribe(paper.readTweets);

  現在,只要joe發出tweet訊息,paper都會得到提醒:

joe.tweet("hated the paper today");

  結果是一個提醒訊息:“Call big meeting! Someone hated the paper today”。

  上面的程式碼,可以在http://www.jspatterns.com/book/7/observer.html地址檢視。

示例#2:鍵盤按鍵遊戲

  讓我們看另一個例子。將重新實現與中介者模式中的鍵盤遊戲完全相同的程式,但是這次使用了觀察者模式。為了使他更先進一些,讓我們接受無限數量的玩家,而不是隻有兩個玩家。仍然使用Player()建構函式建立player物件以及scoreboard物件。不過,mediator現在變成為一個game物件。

  在中介者模式中,mediator物件知曉所有其他參與物件並呼叫它們的方法。觀察者模式中game物件並不會像那樣做。相反,它會讓物件訂閱感興趣的事件。比如,scoreboard物件將會訂閱game的“scorechange”事件。

  讓我們先回顧一下通用publisher物件,然後略微調整它的介面,使之更接近於瀏覽器事件:

  • 並不採用publish()、subscribe()以及unsubscribe()方法,我們採用以fire()、on()以及remove()命名的方法。
  • 事件的type將一直被使用,因此它成為了上述三個函式的第一個引數。
  • 除了訂閱者的函式以外,還會提供一個額外的context,從而支援回撥方法使用this以引用自己的物件。

  新的publisher物件變為:

var publisher = {
    subscribers: {
        any: []
    },
    on: function (type, fn, context) {
        type = type || 'any';
        fn = typeof fn === "function" ? fn : context[fn];
        
        if (typeof this.subscribers[type] === "undefined") {
            this.subscribers[type] = [];
        }
        this.subscribers[type].push({fn: fn, context: context || this});
    },
    remove: function (type, fn, context) {
        this.visitSubscribers('unsubscribe', type, fn, context);
    },
    fire: function (type, publication) {
        this.visitSubscribers('publish', type, publication);
    },
    visitSubscribers: function (action, type, arg, context) {
        var pubtype = type || 'any',
            subscribers = this.subscribers[pubtype],
            i,
            max = subscribers ? subscribers.length : 0;
            
        for (i = 0; i < max; i += 1) {
            if (action === 'publish') {
                subscribers[i].fn.call(subscribers[i].context, arg);
            } else {
                if (subscribers[i].fn === arg && subscribers[i].context === context) {
                    subscribers.splice(i, 1);
                }
            }
        }
    }
};

  新的Player()建構函式變為:

function Player(name, key) {
    this.points = 0;
    this.name = name;
    this.key  = key;
    this.fire('newplayer', this);
}

Player.prototype.play = function () {
    this.points += 1;
    this.fire('play', this);
};

  以上新的部分在於該建構函式接受key,即玩家用於得分所按的鍵盤的鍵(以前的程式碼中將鍵硬編碼到程式中)。另外,每次建立新的player物件時,一個名為“newplayer”的事件將被觸發,每次當玩家玩遊戲的時候,事件“play”將被觸發。

  scoreboard物件保持不變,它只是以當前分值更新其顯示值。

  新的game物件可以記錄所有的player物件,因此它可以產生一個分數並且觸發“scorechange”事件。它還將從瀏覽器中訂閱所有的“keypress”事件,並且知道每個鍵所對應的玩家:

var game = {
    
    keys: {},

    addPlayer: function (player) {
        var key = player.key.toString().charCodeAt(0);
        this.keys[key] = player;
    },

    handleKeypress: function (e) {
        e = e || window.event; // IE
        if (game.keys[e.which]) {
            game.keys[e.which].play();
        }
    },
    
    handlePlay: function (player) {
        var i, 
            players = this.keys,
            score = {};
        
        for (i in players) {
            if (players.hasOwnProperty(i)) {
                score[players[i].name] = players[i].points;
            }
        }
        this.fire('scorechange', score);
    }
};

  可以將任何物件轉變成發行者的函式makePublisher(),仍然與前面報紙訂閱的例子中的對應函式是相同的。game物件變成了一個釋出者(因此,它能夠觸發“scorechange”事件),並且Player.prototype也變成了發行者,以便每個player物件能夠向任何決定監聽的玩家觸發“play”和“newplayer”事件:

makePublisher(Player.prototype);
makePublisher(game);

  game物件訂閱了“play”和“newplayer”事件(此外,還有瀏覽器中的“keypress”事件),而scoreboard則訂閱了“scorechange”事件。

Player.prototype.on("newplayer", "addPlayer", game);
Player.prototype.on("play",      "handlePlay", game);

game.on("scorechange", scoreboard.update, scoreboard);

window.onkeypress = game.handleKeypress;

  正如您在這裡所看到的,on()方法使訂閱者可以指定回撥函式為函式引用(scoreboard.update)或字串(“addPlayer”)的方式。只要提供了上下文環境(比如game物件——on()方法中的context物件),以字串方式提供的回撥函式就能正常執行。

  設定的最後一個小部分是動態建立多個player物件(與它們對應的按鍵一起),使用者想建立多少個player物件都是可以的:

var playername, key;
while (1) {
    playername = prompt("Add player (name)");
    if (!playername) {
        break;
    }
    while (1) {
        key = prompt("Key for " + playername + "?");
        if (key) {
            break;
        }
    }
    new Player(playername,  key);    
}

  到這裡,該遊戲的開發程式就結束了。可以在http://www.jspatterns.com/book/7/observer-game.html檢視完整的原始碼。

  請注意,在中介者模式的實現中,mediator物件必須知道所有其他物件,以便在正確的事件呼叫正確的方法並且與整個遊戲相協調。而在觀察者模式中,game物件顯得更缺乏智慧,它主要依賴於物件觀察某些事件並採取行動。比如,scoreboard監聽“scorechange”事件。這導致了更為鬆散的耦合(越少的物件知道越少),期待駕駛在記錄稅監聽什麼事件時顯得更困難一點。在本例的遊戲中,所有訂閱行為都出現在該程式碼的同一個位置,但是隨著應用程式的增長,on()呼叫可能到處都是(比如,在每個物件的初始化程式碼中)。這會使得該程式難以除錯,因為現在無法僅在單個位置檢視程式碼並理解到底發生了什麼事情。在觀察者模式中,可以擺脫那種從開始一直跟隨到最後的那種過程式順序程式碼執行的程式。

小結

  在本章中,我們學習了一些流行的設計模式以及這些模式在JavaScript中的實現:

  • 單體模式:針對一個“類”僅建立一個物件。
  • 工廠模式:根據字串指定的型別在執行時建立物件的方法。
  • 迭代器模式:提供一個API來遍歷或操縱複雜的自定義資料結構。
  • 裝飾者模式:通過從預定義裝飾者物件中新增功能,從而在執行時調整物件。
  • 策略模式:在選擇最佳策略以處理特定任務(上下文)的時候仍然保持相同的介面。
  • 外觀模式:通常把常用方法包裝到一個新方法中,從而提供一個更為便利的API。
  • 代理模式:通過包裝一個物件以控制它的訪問,其主要方法是將訪問聚集為組或僅當真正必要的時候才執行訪問,從而避免了高昂的操作開銷。
  • 中介者模式:通過使您的物件之間相互並不直接“通話”,而是僅通過一箇中介者物件進行通訊,從而促進形成鬆散耦合。
  • 觀察者模式:通過建立“可觀察的”物件,當發生一個感興趣的事件時可將該事件告知給所有觀察者,從而形成鬆散耦合。

  

  好了,到這裡設計模式的部分就都結束了,設計模式的重要性就不再多說了。下一篇是本書的最後一篇內容:DOM和瀏覽器模式。