《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和浏览器模式。

posted @ 2020-09-02 14:53  Zaking  阅读(232)  评论(0编辑  收藏  举报