六、JavaScript 中介者模式

中介者模式

字典中中介者的定义是,一个中立方,在谈判和冲突解决过程中起辅助作用。在我们的世界,一个中介者是一个行为设计模式,使我们可以导出统一的接口,这样系统不同部分就可以彼此通信。

如果系统组件之间存在大量的直接关系,就可能是时候,使用一个中心的控制点,来让不同的组件通过它来通信。中介者通过将组件之间显式的直接的引用替换成通过中心点来交互的方式,来做到松耦合。这样可以帮助我们解耦,和改善组件的重用性。

在现实世界中,类似的系统就是,飞行控制系统。一个航站塔(中介者)处理哪个飞机可以起飞,哪个可以着陆,因为所有的通信(监听的通知或者广播的通知)都是飞机和控制塔之间进行的,而不是飞机和飞机之间进行的。一个中央集权的控制中心是这个系统成功的关键,也正是中介者在软件设计领域中所扮演的角色。

从实现角度来讲,中介者模式是观察者模式中的共享被观察者对象。在这个系统中的对象之间直接的发布/订阅关系被牺牲掉了,取而代之的是维护一个通信的中心节点。

也可以认为是一种补充-用于应用级别的通知,例如不同子系统之间的通信,子系统本身很复杂,可能需要使用发布/订阅模式来做内部组件之间的解耦。

另外一个类似的例子是DOM的事件冒泡机制,以及事件代理机制。如果系统中所有的订阅者都是对文档订阅,而不是对独立的节点订阅,那么文档就充当一个中介者的角色。DOM的这种做法,不是将事件绑定到独立节点上,而是用一个更高级别的对象负责通知订阅者关于交互事件的信息。

基础的实现

中间人模式的一种简单的实现可以在下面找到,publish()和subscribe()方法都被暴露出来使用:

var mediator = (function(){

    // Storage for topics that can be broadcast or listened to
    var topics = {};

    // Subscribe to a topic, supply a callback to be executed
    // when that topic is broadcast to
    var subscribe = function( topic, fn ){

        if ( !topics[topic] ){
          topics[topic] = [];
        }

        topics[topic].push( { context: this, callback: fn } );

        return this;
    };

    // Publish/broadcast an event to the rest of the application
    var publish = function( topic ){

        var args;

        if ( !topics[topic] ){
          return false;
        }

        args = Array.prototype.slice.call( arguments, 1 );
        for ( var i = 0, l = topics[topic].length; i < l; i++ ) {

            var subscription = topics[topic][i];
            subscription.callback.apply( subscription.context, args );
        }
        return this;
    };

    return {
        publish: publish,
        subscribe: subscribe,
        installTo: function( obj ){
            obj.subscribe = subscribe;
            obj.publish = publish;
        }
    };

}());

高级的实现

对于那些对更加高级实现感兴趣的人,以走读的方式看一看以下我对Jack Lawson优秀的Mediator.js重写的一个缩略版本.在其它方面的改进当中,为我们的中间人支持主题命名空间,用户拆卸和一个更加稳定的发布/订阅系统。但是如果你想跳过这个走读,你可以直接进入到下一个例子继续阅读。

得感谢Jack优秀的代码注释对这部分内容的协助。

首先,让我们实现认购的概念,我们可以考虑一个中间人主题的注册。

通过生成对象实体,我们稍后能够简单的更新认购,而不需要去取消注册然后重新注册它们.认购可以写成一个使用被称作一个选项对象或者一个上下文环境的函数

// Pass in a context to attach our Mediator to.
// By default this will be the window object
(function( root ){

  function guidGenerator() { /*..*/}

  // Our Subscriber constructor
  function Subscriber( fn, options, context ){

    if ( !(this instanceof Subscriber) ) {

      return new Subscriber( fn, context, options );

    }else{

      // guidGenerator() is a function that generates
      // GUIDs for instances of our Mediators Subscribers so
      // we can easily reference them later on. We're going
      // to skip its implementation for brevity

      this.id = guidGenerator();
      this.fn = fn;
      this.options = options;
      this.context = context;
      this.topic = null;

    }
  }
})();

在我们的中间人主题中包涵了一长串的回调和子主题,当中间人发布在我们中间人实体上被调用的时候被启动.它也包含操作数据列表的方法

// Let's model the Topic.
// JavaScript lets us use a Function object as a
// conjunction of a prototype for use with the new
// object and a constructor function to be invoked.
function Topic( namespace ){

  if ( !(this instanceof Topic) ) {
    return new Topic( namespace );
  }else{

    this.namespace = namespace || "";
    this._callbacks = [];
    this._topics = [];
    this.stopped = false;

  }
}

// Define the prototype for our topic, including ways to
// add new subscribers or retrieve existing ones.
Topic.prototype = {

  // Add a new subscriber
  AddSubscriber: function( fn, options, context ){

    var callback = new Subscriber( fn, options, context );

    this._callbacks.push( callback );

    callback.topic = this;

    return callback;
  },

我们的主题实体被当做中间人调用的一个参数被传递.使用一个方便实用的calledStopPropagation()方法,回调就可以进一步被传播开来:

StopPropagation: function(){
  this.stopped = true;
},

我们也能够使得当提供一个GUID的标识符的时候检索订购用户更加容易:

GetSubscriber: function( identifier ){

  for(var x = 0, y = this._callbacks.length; x < y; x++ ){
    if( this._callbacks[x].id == identifier || this._callbacks[x].fn == identifier ){
      return this._callbacks[x];
    }
  }

  for( var z in this._topics ){
    if( this._topics.hasOwnProperty( z ) ){
      var sub = this._topics[z].GetSubscriber( identifier );
      if( sub !== undefined ){
        return sub;
      }
    }
  }

},

接着,在我们需要它们的情况下,我们也能够提供添加新主题,检查现有的主题或者检索主题的简单方法:

AddTopic: function( topic ){
  this._topics[topic] = new Topic( (this.namespace ? this.namespace + ":" : "") + topic );
},

HasTopic: function( topic ){
  return this._topics.hasOwnProperty( topic );
},

ReturnTopic: function( topic ){
  return this._topics[topic];
},

如果我们觉得不再需要它们了,我们也可以明确的删除这些订购用户.下面就是通过它的其子主题递归删除订购用户的代码:

RemoveSubscriber: function( identifier ){

  if( !identifier ){
    this._callbacks = [];

    for( var z in this._topics ){
      if( this._topics.hasOwnProperty(z) ){
        this._topics[z].RemoveSubscriber( identifier );
      }
    }
  }

  for( var y = 0, x = this._callbacks.length; y < x; y++ ) {
    if( this._callbacks[y].fn == identifier || this._callbacks[y].id == identifier ){
      this._callbacks[y].topic = null;
      this._callbacks.splice( y,1 );
      x--; y--;
    }
  }

},

接着我们通过递归子主题将发布任意参数的能够包含到订购服务对象中:

Publish: function( data ){

    for( var y = 0, x = this._callbacks.length; y < x; y++ ) {

        var callback = this._callbacks[y], l;
          callback.fn.apply( callback.context, data );

      l = this._callbacks.length;

      if( l < x ){
        y--;
        x = l;
      }
    }

    for( var x in this._topics ){
      if( !this.stopped ){
        if( this._topics.hasOwnProperty( x ) ){
          this._topics[x].Publish( data );
        }
      }
    }

    this.stopped = false;
  }
};

接着我们暴露我们将主要交互的调节实体.这里它是通过注册的并且从主题中删除的事件来实现的

function Mediator() {

  if ( !(this instanceof Mediator) ) {
    return new Mediator();
  }else{
    this._topics = new Topic( "" );
  }

};

想要更多先进的用例,我们可以看看调解支持的主题命名空间,下面这样的asinbox:messages:new:read.GetTopic 返回基于一个命名空间的主题实体。

Mediator.prototype = {

  GetTopic: function( namespace ){
    var topic = this._topics,
        namespaceHierarchy = namespace.split( ":" );

    if( namespace === "" ){
      return topic;
    }

    if( namespaceHierarchy.length > 0 ){
      for( var i = 0, j = namespaceHierarchy.length; i < j; i++ ){

        if( !topic.HasTopic( namespaceHierarchy[i]) ){
          topic.AddTopic( namespaceHierarchy[i] );
        }

        topic = topic.ReturnTopic( namespaceHierarchy[i] );
      }
    }

    return topic;
  },

这一节我们定义了一个Mediator.Subscribe方法,它接受一个主题命名空间,一个将要被执行的函数,选项和又一个在订阅中调用函数的上下文环境.这样就创建了一个主题,如果这样的一个主题存在的话

Subscribe: function( topiclName, fn, options, context ){
  var options = options || {},
      context = context || {},
      topic = this.GetTopic( topicName ),
      sub = topic.AddSubscriber( fn, options, context );

  return sub;
},

根据这一点,我们可以进一步定义能够访问特定订阅用户,或者将他们从主题中递归删除的工具

// Returns a subscriber for a given subscriber id / named function and topic namespace

GetSubscriber: function( identifier, topic ){
  return this.GetTopic( topic || "" ).GetSubscriber( identifier );
},

// Remove a subscriber from a given topic namespace recursively based on
// a provided subscriber id or named function.

Remove: function( topicName, identifier ){
  this.GetTopic( topicName ).RemoveSubscriber( identifier );
},

我们主要的发布方式可以让我们随意发布数据到选定的主题命名空间,这可以在下面的代码中看到。

主题可以被向下递归.例如,一条对inbox:message的post将发送到inbox:message:new和inbox:message:new:read.它将像接下来这样被使用:Mediator.Publish( "inbox:messages:new", [args] );

Publish: function( topicName ){
    var args = Array.prototype.slice.call( arguments, 1),
        topic = this.GetTopic( topicName );

    args.push( topic );

    this.GetTopic( topicName ).Publish( args );
  }
};

最后,我们可以很容易的暴露我们的中间人,将它附着在传递到根中的对象上:

 root.Mediator = Mediator;
  Mediator.Topic = Topic;
  Mediator.Subscriber = Subscriber;

// Remember we can pass anything in here. I've passed inwindowto
// attach the Mediator to, but we can just as easily attach it to another
// object if desired.
})( window );

示例
无论是使用来自上面的实现(简单的选项和更加先进的选项都是),我们能够像下面这样将一个简单的聊天记录系统整到一起:

HTML
<h1>Chat</h1>
<form id="chatForm">
    <label for="fromBox">Your Name:</label>
    <input id="fromBox" type="text"/>
    <br />
    <label for="toBox">Send to:</label>
    <input id="toBox" type="text"/>
    <br />
    <label for="chatBox">Message:</label>
    <input id="chatBox" type="text"/>
    <button type="submit">Chat</button>
</form>

<div id="chatResult"></div>
Javascript
$( "#chatForm" ).on( "submit", function(e) {
    e.preventDefault();

    // Collect the details of the chat from our UI
    var text = $( "#chatBox" ).val(),
        from = $( "#fromBox" ).val(),
        to = $( "#toBox" ).val();

    // Publish data from the chat to the newMessage topic
    mediator.publish( "newMessage" , { message: text, from: from, to: to } );
});

// Append new messages as they come through
function displayChat( data ) {
    var date = new Date(),
        msg = data.from + " said \"" + data.message + "\" to " + data.to;

    $( "#chatResult" )
        .prepend("
<p>
    " + msg + " (" + date.toLocaleTimeString() + ")
</p>
");
}

// Log messages
function logChat( data ) {
    if ( window.console ) {
        console.log( data );
    }
}

// Subscribe to new chat messages being submitted
// via the mediator
mediator.subscribe( "newMessage", displayChat );
mediator.subscribe( "newMessage", logChat );

// The following will however only work with the more advanced implementation:

function amITalkingToMyself( data ) {
    return data.from === data.to;
}

function iAmClearlyCrazy( data ) {
    $( "#chatResult" ).prepend("
<p>
    " + data.from + " is talking to himself.
</p>
");
}

 mediator.Subscribe( amITalkingToMyself, iAmClearlyCrazy );

优点&缺点

中间人模式最大的好处就是,它节约了对象或者组件之间的通信信道,这些对象或者组件存在于从多对多到多对一的系统之中。由于解耦合水平的因素,添加新的发布或者订阅者是相对容易的。

也许使用这个模式最大的缺点是它可以引入一个单点故障。在模块之间放置一个中间人也可能会造成性能损失,因为它们经常是间接地的进行通信的。由于松耦合的特性,仅仅盯着广播很难去确认系统是如何做出反应的。

这就是说,提醒我们自己解耦合的系统拥有许多其它的好处,是很有用的——如果我们的模块互相之间直接的进行通信,对于模块的改变(例如:另一个模块抛出了异常)可以很容易的对我们系统的其它部分产生多米诺连锁效应。这个问题在解耦合的系统中很少需要被考虑到。

在一天结束的时候,紧耦合会导致各种头痛,这仅仅只是另外一种可选的解决方案,但是如果得到正确实现的话也能够工作得很好。

中间人VS观察者

开发人员往往不知道中间人模式和观察者模式之间的区别。不可否认,这两种模式之间有一点点重叠,但让我们回过头来重新寻求GoF的一种解释:

“在观察者模式中,没有封装约束的单一对象”。取而代之,观察者和主题必须合作来维护约束。通信的模式决定于观察者和主题相互关联的方式:一个单独的主题经常有许多的观察者,而有时候一个主题的观察者是另外一个观察者的主题。“

中间人和观察者都提倡松耦合,然而,中间人默认使用让对象严格通过中间人进行通信的方式实现松耦合。观察者模式则创建了观察者对象,这些观察者对象会发布触发对象认购的感兴趣的事件。

中间人VS门面

不久我们的描述就将涵盖门面模式,但作为参考之用,一些开发者也想知道中间人和门面模式之间有哪些相似之处。它们都对模块的功能进行抽象,但有一些细微的差别。

中间人模式让模块之间集中进行通信,它会被这些模块明确的引用。门面模式却只是为模块或者系统定义一个更加简单的接口,但不添加任何额外的功能。系统中其他的模块并不直接意识到门面的概念,而可以被认为是单向的。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 218,386评论 6 506
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 93,142评论 3 394
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 164,704评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,702评论 1 294
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,716评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,573评论 1 305
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,314评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,230评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,680评论 1 314
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,873评论 3 336
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,991评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,706评论 5 346
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,329评论 3 330
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,910评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,038评论 1 270
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,158评论 3 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,941评论 2 355

推荐阅读更多精彩内容

  • javascript设计模式与开发实践 设计模式 每个设计模式我们需要从三点问题入手: 定义 作用 用法与实现 单...
    穿牛仔裤的蚊子阅读 4,062评论 0 13
  • 工厂模式类似于现实生活中的工厂可以产生大量相似的商品,去做同样的事情,实现同样的效果;这时候需要使用工厂模式。简单...
    舟渔行舟阅读 7,761评论 2 17
  • 设计模式基本原则 开放-封闭原则(OCP),是说软件实体(类、模块、函数等等)应该可以拓展,但是不可修改。开-闭原...
    西山薄凉阅读 3,798评论 3 14
  • 工厂模式 单体模式 模块模式 代理模式 职责链模式 命令模式 模板方法模式 策略模式 发布-订阅模式 中介者模式 ...
    HelloJames阅读 1,008评论 0 6
  • 想哭就哭吧 世界不会动容 只当是一次梳洗 谁的姿态比谁的更美 谁的眼泪比谁的更贵 不要在意 沾湿了谁的衣襟 叩开了...
    空谷幽兰_9d14阅读 322评论 2 10