在我们生活的世界中,每个人每个物体之间都会产生一些错综复杂的联系。在应用程序里也一样,程序由大大小小的单一对象组成,所有这些对象都按照某种关系和规则来通信。

平时我们大概能记住10个朋友的电话、30家餐馆的位置。在程序里,也许一个对象会和其他10个对象打交道,所以它会保持10个对象的引用。当程序的规模增大,对象会越来越多,它们之间的关系也越来越复杂,难免会形成网状的交叉引用。当我们改变或删除其中个对象的时候,很可能需要通知所有引用到它的对象。这样一来,就像在心脏旁边拆掉一根毛细血管一般,唯一点很小的修改也必须小心翼翼。

image-20241008211245435

面向对象设计鼓励将行为分布到各个对象中,把对象划分成更小的粒度,有助于增强对象的可复用性,但由于这些细粒度对象之间的联系激增,又有可能会反过来降低它们的可复用性。

中介者模式的作用就是解除对象与对象之间的紧耦合关系。增加一个中介者对象后,所有的相关对象都通过中介者对象来通信,而不是互相引用,所以当一个对象发生改变时,只需要通知中介者对象即可。中介者使各对象之间耦合松散,而且可以独立地改变它们之间的交互。中介者模式使网状的多对多关系变成了相对简单的一对多关系。

image-20241008223351989

一、现实中的中介者

  1. 机场指挥塔

    中介者也被称为调停者,我们想象一下机场的指挥塔,如果没有指挥塔的存在,每架飞机要和方圆100公里内的所有飞机通信,才能确定航线以及飞行状况,后果是不可想象的。现实中的情况是,每架飞机都只需要和指挥塔通信。指挥塔作为调停者,知道每一架飞机的飞行状况,所以它可以安排所有飞机的起降时间,及时做出航线调整。

  2. 博彩公司

    打麻将的人经常遇到这样的问题,打了几局之后开始计算钱,A自摸了两把,B杠了三次,C点炮一次给D,谁应该给谁多少钱已经很难计算清楚,而这还是在只有4个人参与的情况下。

    在世界杯期间购买足球彩票,如果没有博彩公司作为中介,上千万的人一起计算赔率和输赢绝对是不可能实现的事情。有了博彩公司作为中介,每个人只需和博彩公司发生关联,博彩公同会根据所有人的投注情况计算好赔率,彩民们赢了钱就从博彩公司拿,输了钱就交给博彩公司。

二、中介者模式的例子——购买商品

假设我们正在编写一个手机购买的页面,在购买流程中,可以选择手机的颜色以及输人购买数量,同时页面中有两个展示区域,分别向用户展示刚刚选择好的颜色和数量。还有一个按钮动态显示下一步的操作,我们需要查询该颜色手机对应的库存,如果库存数量少于这次的购买数量,按钮将被禁用并且显示库存不足,反之按钮可以点击并且显示放入购物车。

这个需求是非常容易实现的,假设我们已经提前从后台获取到了所有颜色手机的库存量:

var goods = {
"red":3,
"blue":6
};

那么页面有可能显示为如下几种场景:

  • 选择红色手机,购买4个,库存不足。
  • 选择蓝色手机,购买5个,库存充足,可以加入购物车。
  • 或者是没有输入购买数量的时候,按钮将被禁用并显示相应提示。

我们大概已经能够猜到,接下来将遇到至少5个节点,分别是:

  • 下拉选择框 colorSelect
  • 文本输入框 numberInput
  • 展示颜色信息 colorInfo
  • 展示购买商品数量信息 numberInfo
  • 决定下一步操作的按钮 nextBtn
  1. 开始编写代码

    从编写HTML开始:

    <body>
    选择颜色:
    <select id="colorSelect">
    <option value="">请选择</option>
    <option value="red">红色</option>
    <option value="blue">蓝色</option>
    </select>

    输入购买数量:
    <input type="text" id="numberInput" />

    选择颜色:
    <div id="colorInfo"></div><br />

    输入数量:
    <div id="numberInfo"></div>

    <button id="nextBtn" disable="true">请选择手机颜色和购买数量</button>
    </body>

    接下来监听colorSelect的onchange事件函数和numberInput的oninput事件函数,然后在这两个事件中作出相应的处理。

    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, // 数量
    stock = goods[color]; // 该颜色手机对应的当前库存

    colorInfo.innerHTML = color;

    if(!color){
    nextBtn.disabled = true;
    nextBtn.innerHTML = '请选择手机颜色';
    return;
    }
    if(Number.isInter(number - 0) && number > 0){
    nextBtn.disabled = true;
    nextBtn.innerHTML = '请输入正确的购买数量';
    return;
    }
    if(number > stock){
    nextBtn.disabled = true;
    nextBtn.innerHTML = '库存不足';
    return;
    }
    nextBtn.disabled = false;
    nextBtn.innerHTML = '放入购物车';
    };
  2. 对象之间的联系

    来考虑一下,当触发了colorselect 的onchange之后,会发生什么事情。

    首先我们要让colorInfo中显示当前选中的颜色,然后获取用户当前输人的购买数量,对用户的输人值进行一些合法性判断。再根据库存数量来判断nextBtn的显示状态。

    别忘了,还要编写numberInput的事件相关代码:

    numbernput.oninput = function(){
    var color = colorSelect.value, // 颜色
    number = this.value, // 数量
    stock = goods[color ]; //该颜色子机对应的当前库存

    numberInfo.innerHML = number;

    if(!color ){
    nextBtn.disabled = true;
    nextBtn.innerHTM = '请选择手机颜色';
    return;
    }

    if(((number - 0) | 0) !== number - 0){
    nextBtn.disabled = true;
    nextBtn.innerHTM = '请输入正确的购买数量';
    return;
    }
    if(number > stock){
    nextBtn.disabled = true;
    nextBtn.innerHTML = '库存不足';
    return;
    }
    nextBtn.disabled = false;
    nextBtn.innerHTML = '放入购物车';

    }
  3. 可能遇到的困难

    虽然目前顺利完成了代码编写,但随之而来的需求改变有可能给我们带来麻烦。假设现在要求去掉colorInfo和numberInfo这两个展示区域,我们就要分别改动 colorselect.onchange和numberInput. oninput里面的代码,因为在先前的代码中,这些对象确实是耦合在一起的。

    目前我们面临的对象还不算太多,当这个页面里的节点激曾到10个或者15个时,它们之间的联系可能变得更加错综复杂,任何一次改动都将变得很棘手。为了证实这一点,我们假设页面中将新增另外一个下拉选择框,代表选择手机内存。现在我们需要计算颜色、内存和购买数量,来判断nextBtn是显示库存不足还是放人购物车。

    首先我们要增加两个HTML汇节点:

    <body>
    选择颜色:
    <select id="colorSelect">
    <option value="">请选择</option>
    <option value="red">红色</option>
    <option value="blue">蓝色</option>
    </select>

    选择内存:
    <select id="memorySelect">
    <option value="">请选择</option>
    <option value="32G">32G</option>
    <option value="64G">64G</option>
    </select>

    输入购买数量:
    <input type="text" id="numberInput" />

    选择颜色:
    <div id="colorInfo"></div><br />

    选择内存:
    <div id="memoryInfo"></div><br />

    输入数量:
    <div id="numberInfo"></div>

    <button id="nextBtn" disable="true">请选择手机颜色和购买数量</button>
    </body>
    var colorSelect = document.getElementById('colorSelect'),
    numberInput = document.getElementById('numberInput'),
    colorInfo = document.getElementById('colorInfo'),
    numberInfo = document.getElementById('numberInfo'),
    memoryInfo = document.getElementById('memoryInfo'),
    nextBtn = document.getElementById('nextBtn');

    接下来修改表示库存的JSON对象以及修改colorSelect的onchange事件函数:

    var goods = {
    "red|3G": 3,
    "red|16G": 0,
    "blue|32G": 1,
    "blue|16G": 6
    };

    colorSelect.onchange = function(){
    var color = this.value;
    var memory = memorySelect.value;
    var stock = goods[color + '|' + memory];

    number = numberInput.value,
    colorInfo.innerHTML = color;

    if(!color){
    nextBtn.disabled = true;
    nextBtn.innerHTML = '请选择手机颜色';
    return;
    }

    if(!memory){
    nextBtn.disabled = true;
    nextBtn.innerHTML = '请选择内存大小';
    return;
    }

    if(Number.isInteger(number - 0) && number > 0){
    nextBtn.disabled = true;
    nextBtn.innerHTML = '请输入正确的购买数量';
    return;
    }

    if(number > stock){
    nextBtn.disabled = true;
    nextBtn.innerHTML = '库存不足';
    return;
    }

    nextBtn.disabled = false;
    nextBtn.innerHTML = '放入购物车';

    }

    当然同样改写numberInput的事件相关代码,具体代码的改变跟colorSelect大同小异,最后还要新增memorySelect的onchange事件函数:

    memorySelect.onchange = function(){
    var color = colorSelect.value,
    memory = this.value,
    memory = this.value,
    stock = goods[color + '|' + memory];
    memoryInfo.innerHTML = memory;

    if(!color){
    nextBtn.disabled = true;
    nextBtn.innerHTML = '请选择手机颜色';
    return;
    }

    if(!memory){
    nextBtn.disabled = true;
    nextBtn.innerHTML = '请选择内存大小';
    return;
    }

    if(Number.isInteger(number - 0) && number > 0){
    nextBtn.disabled = true;
    nextBtn.innerHTML = '请输入正确的购买数量';
    return;
    }

    if(number > stock){
    nextBtn.disabled = true;
    nextBtn.innerHTML = '库存不足';
    return;
    }

    nextBtn.disabled = false;
    nextBtn.innerHTML = '放入购物车';
    }

    我们仅仅是增加了一个内存的选择条件,就要改变如此多的代码,这是因为在目前的实现中,每个节点对象都是耦合在一起的,改变或者增加任何一个节点对象,都要通知到与其相关的对象。

  4. 引入中介者

    现在引入中介者对象,所有的节点对象只跟中介者通信。当下拉框colorSelect、memorySelect和文本输入框numberInput发生了事件行为时,它们仅仅通知中介者它们被改变了,同时把自身当作参数传入中介者,以便中介者辨别是谁发生了改变。剩下的所有事情都交给中介者对象来完成,这样一来,无论是修改还是新增节点,都只需改动中介者对象里的代码。

    var goods = {
    "red|3G": 3,
    "red|16G": 0,
    "blue|32G": 1,
    "blue|16G": 6
    };

    var mediator = (function(){
    var colorSelect = document.getElementsById('colorSelect'),
    memorySelect = document.getElementsById('memorySelect'),
    colorInfo = document.getElementsById('colorInfo'),
    memoryInfo = document.getElementsById('memoryInfo'),
    numberInfo = document.getElementsById('numberInfo'),
    nextBtn = document.getElementsById('nextBtn');

    return {
    changed: function(obj){
    var color = colorSelect.value,
    memory = memorySelect.value,
    number = numberInput.value,
    stock = goods[color + '|' + memory];
    if(obj === colorSelect){
    colorInfo.innerHTML = color;
    }else if(obj === memorySelect){
    memoryInfo.innerHTML = memory;
    }else if(obj === numberInput){
    numberInfo.innerHTML = number;
    }

    if(!memory){
    nextBtn.disabled = true;
    nextBtn.innerHTML = '请选择内存大小';
    return;
    }

    if(Number.isInteger(number - 0) && number > 0){
    nextBtn.disabled = true;
    nextBtn.innerHTML = '请输入正确的购买数量';
    return;
    }

    if(number > stock){
    nextBtn.disabled = true;
    nextBtn.innerHTML = '库存不足';
    return;
    }

    nextBtn.disabled = false;
    nextBtn.innerHTML = '放入购物车';
    }
    }
    })();

    // 事件函数
    colorSelect.onchange = function(){
    mediator.changed(this);
    };
    memorySelect.onchange = function(){
    mediator.changed(this);
    };
    numberInput.onInput = function(){
    mediator.changed(this);
    };

    可以想象,某天我们又要新增一些跟需求相关的节点,比如CPU型号,那我们只需要稍稍改动mediator对象即可:

    var goods = {
    "red|3G": 3,
    "red|16G": 0,
    "blue|32G": 1,
    "blue|16G": 6
    };

    var mediator = (function(){
    var cpuSelect = document.getElementById('cpuSelect');
    return {
    change: function(obj){
    var cpu = cpuSelect.value,
    stock = goods[color + '|' + momory + '|' +cpu];
    if(obj === cpuSelect){
    cpuInfo.innerHTML = cpu;
    }
    }
    }
    })()

三、小结

中介者模式是迎合迪米特法则的一种实现。迪米特法则也叫最少知识原则,是指一个对象应该尽可能少地了解另外的对象(类似不和陌生人说话)。如果对象之间的拥合性太高,一个对象发生改变之后,难免会影响到其他的象,跟“城门失火,殃及池鱼”的道理是一样的。而在中介者模式里,对象之间几乎不知道彼此的存在,它们只能通过中介者对象来互相影响对方。

因此,中介者模式使各个对象之间得以解耦,以中介者和对象之间的一对多关系取代了对象之间的网状多对多关系。各个对象只需关注自身功能的实现,对象之间的交互关系交给了中介者对象来实现和维护。

不过,中介者模式也存在一些缺点。其中,最大的缺点是系统中会新增一个中介者对象,因为对象之间交互的复杂性,转移成了中介者对象的复杂性,使得中介者对象经常是巨大的。中介者对象自身往往就是一个难以维护的对象。

我们都知道,毒贩子虽然使吸毒者和制毒者之间的耦合度降低,但毒贩子也要抽走一部分利润。同样,在程序中,中介者对象要占去一部分内存。而且毒贩本身还要防止被警察抓住,因为它了解整个犯罪链条中的所有关系,这表明中介者对象自身往往是一个难以维护的对象。

中介者模式可以非常方便地对模块或者对象进行解耦,但对象之间并非一定需要解耦。在实际项目中,模块或对象之间有一些依赖关系是很正常的。毕竟我们写程序是为了快速完成项目交付生产,而不是堆砌模式和过度设计。关键就在于如何去衡量对象之间的耦合程度。一般来说,如果对象之间的复杂耦合确实导致调用和维护出现了困难,而且这些耦合度随项目的变化呈指数增长曲线,那我们就可以考虑用中介者模式来重构代码。