《JavaScript设计模式》——9.6 Mediator(中介者)模式

简介:

本节书摘来自异步社区《JavaScript设计模式》一书中的第9章,第9.6节, 作者: 【美】Addy Osmani 译者: 徐涛 更多章节内容可以访问云栖社区“异步社区”公众号查看。

9.6 Mediator(中介者)模式

在字典里,中介者是指“协助谈判和解决冲突的中立方”1。在本书设计模式里,中介者是一种行为设计模式,它允许我们公开一个统一的接口,系统的不同部分可以通过该接口进行通信。

如果一个系统的各个组件之间看起来有太多的直接关系,也许是时候需要一个中心控制点了,以便各个组件可以通过这个中心控制点进行通信。Mediator模式促进松散耦合的方式是:确保组件的交互是通过这个中心点来处理的,而不是通过显式地引用彼此。这种模式可以帮助我们解耦系统并提高组件的可重用性。

现实世界的一个例子是典型的机场交通控制系统。机场控制塔(中介者)处理飞机的起飞和降落,因为所有通信(监听到或发出的通知)都是从飞机到控制塔,而不是飞机和飞机直接相互通信的。中央控制系统是该系统成功的关键,而这才是中介者在软件设计中所担任的真正角色(图9-5)。
screenshot

就实现而言,Mediator模式本质上是Observer模式的共享目标。它假设该系统中对象或模块之间的订阅和发布关系被牺牲掉了,从而维护中心联络点。

它也可能被认为是额外的或者是用于应用程序间的通知,如不同子系统之间的通信,这些子系统本身就很复杂,且可能希望通过发布/订阅关系实现内部组件间的解耦。

另一个例子是 DOM 事件冒泡和事件委托。如果系统中所有的订阅针对的是文档document而不是单个node节点,则这个文档会有效地充当中介者。更高级别(level)的对象承担了向订阅者通知有关交互事件的责任,而不是绑定到单个节点的事件。
**
9.6.1 基本实现**
可以在下面找到Mediator模式的简单实现,暴露了publish()和subscribe()方法来使用:

var mediator = (function (){
// 存储可被广播或监听的topic
var topics = {};
// 订阅一个topic,提供一个回调函数,一旦topic被广播就执行该回调
var subscribe = function (topic, fn){
     if (!topics[topic]){
       topics[topic] = [];
     }
     topics[topic].push({ context: this, callback: fn });
     returnthis;
};
// 发布/广播事件到程序的剩余部分
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;
     }
};
})();

9.6.2 高级实现
如果你对更高级的代码实现感兴趣,深入下去可以浏览到 Jack Lawson 的优秀Mediator.js(//thejacklawson.com/Mediator.js/)的简洁版。除了其他改进以外,这个版本支持 topic 命名空间、订阅者删除和用于中介者的更强大的发布/订阅(Publish/Subscribe)系统。但如果你想跳过这些内容,则可以直接进入下一个示例继续阅读。2

首先,让我们来实现订阅者的概念,可以考虑一个Mediator的topic注册实例。

通过生成对象实例,之后我们可以很容易地更新订阅者,而不需要注销并重新注册它们。订阅者可以写成构造函数,该函数接受三个参数:一个可被调用的函数fn、一个options对象和一个context(上下文)。

// 将context上下文传递给订阅者,默认上下文是window对象
(function (root){
  function guidGenerator() { /*..*/ }
  // 订阅者构造函数
  function Subscriber(fn, options, context) {
     if (!this instanceof Subscriber) {
      retur nnew Subscriber(fn, context, options);
    } else {
      // guidGenerator()是一个函数,用于为订阅者生成GUID,以便之后很方便地引用它们。
      // 为了简洁,跳过具体实现
      this.id = guidGenerator();
      this.fn = fn;
      this.options = options;
      this.context = context;
      this.topic = null;
    }
  }
})();

Mediator中的topic持有了一组回调函数和子topic列表,一旦Mediator.Publish方法在Mediator实例上被调用时,这些回调函数就会被触发。它还包含用于操作数据列表的方法。

// 模拟Topic
// JavaScript允许我们使用Function对象作为原型的结合与新对象和构造函数一起调用

function Topic( namespace ){
  if ( !this instanceof Topic ) {
     return new Topic( namespace );
  }else{
    this.namespace = namespace || "";
    this._callbacks = [];
    this._topics = [];
    this.stopped = false;
  }
}

// 定义topic的prototype原型,包括添加订阅者和获取订阅者的方式

Topic.prototype = {
  // 添加新订阅者
  AddSubscriber: function( fn, options, context ){
    var callback = new Subscriber( fn, options, context );
    this._callbacks.push( callback );
    callback.topic = this;
    return callback;
    },

我们的Topic实例作为一个参数传递给Mediator回调。然后可以StopPropagation()的简便方法来调用进一步的回调传播:

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

接下来,如果需要它们,我们可以提供简单方法来添加新topic、检查现有topic或者获取topic:

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

接下来,我们将通过子topic递归向订阅者发布(Publish)任意参数。

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

这里暴露了我们将主要与之交互的Mediator实例。在这里,完成了事件在topic上的注册和移除。

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

对于更高级的使用场景,我们可以让Mediator支持用于inbox:messages:new:read等主题topic的命名空间。在接下来的示例中,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方法,它接受一个topic命名空间、一个可执行的fn函数、options,以及调用该函数的context上下文。如果topic不存在,则创建一个。

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

继续下去,我们可以进一步定义用于访问特定订阅者或将订阅者从topic中递归删除的实用程序。

// 通过给定的订阅者ID/命名函数和topic命名空间返回一个订阅者

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

// 通过给定的订阅者ID或命名函数,从给定的topic命名空间递归删除订阅者

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

主要的Publish方法允许我们向所选择的topic命名空间任意发布数据。

Topic向下递归调用。例如,一个发往inbox:messages的帖子将被发至inbox:messages: new和inbox:messages: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 );
}
  };

最后,我们可以很容易地将Mediator作为一个对象附加到root上:

root.Mediator = Mediator;
Mediator.Topic = Topic;
Mediator.Subscriber = Subscriber;
// 记住,这里可以传递任何内容。这里我传递了window对象作为Mediator的附加对象,但也可以随时附加到其他对象上。
})( window );

9.6.3 示例
通过使用上述的任一种实现(简单的和高级的实现),我们可以建立一个简单的聊天记录系统,如下所示。

如下是HTML代码:
screenshot

如下是JavaScript代码:

$("#chatForm").on("submit", function (e) {
    e.preventDefault();
   // 从UI上获取chat的数据
   var text = $("#chatBox").val(),
       from = $("#fromBox").val();
       to = $("#toBox").val();

// 将数据发布到newMessage主题上

mediator.publish("newMessage", { message: text, from: from, to: to });
});

// 将新消息附加到聊天结果记录上

function displayChat(data) {
     var date = new Date(),
         msg = data.from + " said \"" + data.message + "\" to " + data.to;
    $("#chatResult")
    .prepend("" + msg + " (" + date.toLocaleTimeString() + ")");
}

// 记录消息日志

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

// 通过mediator订阅新提交的newMessage主题

mediator.subscribe("newMessage", displayChat);
mediator.subscribe("newMessage", logChat);

// 如下代码仅在高级代码实现上可以使用

function amITalkingToMyself(data) {
     return data.from === data.to;
}
function iAmClearlyCrazy(data) {
    $("#chatResult").prepend("" + data.from + " is talking to himself.");
}
mediator.Subscribe(amITalkingToMyself, iAmClearlyCrazy);

9.6.4 优点和缺点
Mediator模式的最大好处是:它能够将系统中对象或组件之间所需的通信渠道从多对多减少到多对一。由于现在的解耦程度较高,添加新发布者和订阅者相对也容易多了。

或许使用这种模式最大的缺点是:它会引入单一故障点。将Mediator放置于模块之间可以导致性能下降,因为它们总是间接地进行通信。由于松耦合的性质,很难通过仅关注广播来确定一个系统如何作出反应。

也就是说,自我提醒解耦的系统有很多其他的优点:如果模块之间直接相互通信,模块的改变(如另一个模块抛出一个异常)容易让应用程序的其余部分产生多米诺效应。这个问题对解耦的系统来说就不是个大问题。

最后,紧密耦合会引起各种各样的问题,这只是另一个替代方案,但如果正确地实现,它也能很好地工作。

9.6.5 中介者(Mediator)与观察者(Observer)
开发人员通常想知道Mediator模式和Observer模式之间的差异是什么。无可否认地,它们之间有一些重叠,让我们重新参考“四人组”作出的解释:

在Observer模式中,不存在封装约束的单一对象。Observer和Subject(目标)必须合作才能维持约束。Communication(通信)模式由观察者和目标互连的方式所决定:单一目标通常有很多观察者,有时一个目标的观察者是另一个观察者的目标。
Mediator和Observer都能够促进松耦合;然而,Mediator模式通过限制对象严格通过Mediator进行通信来实现这一目的。Observer模式创建观察者对象,观察者对象向订阅它们的对象发布其感兴趣的事件。

9.6.6 中介者(Mediator)与外观(Facade)
我们将简单提一下Facade模式,但出于引用目的,一些开发人员可能也想知道Mediator和Facade模式之间是否有相似点。它们都能够抽象出现有模块的功能,但是也有一些细微的差别。

Mediator模块在它被模块显式引用的地方汇集这些模块之间的通信。从某种意义上说,这是多方向的。另一方面,Facade模式仅是为模块或系统定义了一个较简单的接口,而没有添加任何额外的功能。系统中的其他模块不会直接关联外观,所以可以被视为单向的。

相关文章
|
1月前
|
设计模式 安全 Java
Kotlin教程笔记(51) - 改良设计模式 - 构建者模式
Kotlin教程笔记(51) - 改良设计模式 - 构建者模式
|
1月前
|
设计模式 开发者 Python
Python编程中的设计模式:工厂方法模式###
本文深入浅出地探讨了Python编程中的一种重要设计模式——工厂方法模式。通过具体案例和代码示例,我们将了解工厂方法模式的定义、应用场景、实现步骤以及其优势与潜在缺点。无论你是Python新手还是有经验的开发者,都能从本文中获得关于如何在实际项目中有效应用工厂方法模式的启发。 ###
|
20天前
Next.js 实战 (三):优雅的实现暗黑主题模式
这篇文章介绍了在Next.js中实现暗黑模式的具体步骤。首先,需要安装next-themes库。然后,在/components/ThemeProvider/index.tsx文件中新增ThemeProvider组件,并在/app/layout.tsx文件中注入该组件。如果想要加入过渡动画,可以修改代码实现主题切换时的动画效果。最后,需要在需要的位置引入ThemeModeButton组件,实现暗黑模式的切换。
|
1月前
|
设计模式 前端开发 JavaScript
JavaScript设计模式及其在实战中的应用,涵盖单例、工厂、观察者、装饰器和策略模式
本文深入探讨了JavaScript设计模式及其在实战中的应用,涵盖单例、工厂、观察者、装饰器和策略模式,结合电商网站案例,展示了设计模式如何提升代码的可维护性、扩展性和可读性,强调了其在前端开发中的重要性。
35 2
|
1月前
|
设计模式 安全 Java
Kotlin - 改良设计模式 - 构建者模式
Kotlin - 改良设计模式 - 构建者模式
|
1月前
|
前端开发 JavaScript UED
探索JavaScript的异步编程模式
【10月更文挑战第40天】在JavaScript的世界里,异步编程是一道不可或缺的风景线。它允许我们在等待慢速操作(如网络请求)完成时继续执行其他任务,极大地提高了程序的性能和用户体验。本文将深入浅出地探讨Promise、async/await等异步编程技术,通过生动的比喻和实际代码示例,带你领略JavaScript异步编程的魅力所在。
30 1
|
1月前
|
设计模式 安全 Java
Kotlin教程笔记(51) - 改良设计模式 - 构建者模式
Kotlin教程笔记(51) - 改良设计模式 - 构建者模式
45 1
|
2月前
|
前端开发 JavaScript UED
探索JavaScript中的异步编程模式
【10月更文挑战第21天】在数字时代的浪潮中,JavaScript作为一门动态的、解释型的编程语言,以其卓越的灵活性和强大的功能在Web开发领域扮演着举足轻重的角色。本篇文章旨在深入探讨JavaScript中的异步编程模式,揭示其背后的原理和实践方法。通过分析回调函数、Promise对象以及async/await语法糖等关键技术点,我们将一同揭开JavaScript异步编程的神秘面纱,领略其带来的非阻塞I/O操作的魅力。让我们跟随代码的步伐,开启一场关于时间、性能与用户体验的奇妙之旅。
|
1月前
|
前端开发 JavaScript UED
探索JavaScript的异步编程模式
【10月更文挑战第33天】在JavaScript的世界里,异步编程是提升应用性能和用户体验的关键。本文将带你深入理解异步编程的核心概念,并展示如何在实际开发中运用这些知识来构建更流畅、响应更快的Web应用程序。从回调函数到Promises,再到async/await,我们将一步步解锁JavaScript异步编程的秘密,让你轻松应对各种复杂的异步场景。
|
3月前
|
设计模式 数据库连接 PHP
PHP中的设计模式:提升代码的可维护性与扩展性在软件开发过程中,设计模式是开发者们经常用到的工具之一。它们提供了经过验证的解决方案,可以帮助我们解决常见的软件设计问题。本文将介绍PHP中常用的设计模式,以及如何利用这些模式来提高代码的可维护性和扩展性。我们将从基础的设计模式入手,逐步深入到更复杂的应用场景。通过实际案例分析,读者可以更好地理解如何在PHP开发中应用这些设计模式,从而写出更加高效、灵活和易于维护的代码。
本文探讨了PHP中常用的设计模式及其在实际项目中的应用。内容涵盖设计模式的基本概念、分类和具体使用场景,重点介绍了单例模式、工厂模式和观察者模式等常见模式。通过具体的代码示例,展示了如何在PHP项目中有效利用设计模式来提升代码的可维护性和扩展性。文章还讨论了设计模式的选择原则和注意事项,帮助开发者在不同情境下做出最佳决策。