四、行为型模式(2/2)
职责链模式:领导,我想请个假
1. 你曾经见过的职责链模式
小伙伴来你的城市找你玩耍,因此你需要请两天假。首先跟你的小组领导提了一句,小领导说不行呐我只能批半天假,建议找部门经理。于是你来到了部门经理办公室,部门经理说不行呐我只能批一天假,建议找总经理。来到总经理办公室,总经理勉为其难的说,好叭,不过要扣你四天工资。于是你请到了两天假,和小伙伴快乐(并不 )玩耍了。
当你作为请求者提出请假申请时,这个申请会由小组领导、部门经理、总经理之中的某一位领导来进行处理,但一开始提出申请的时候,并不知道这个申请之后由哪个领导来处理,也许是部门经理,或者是总经理,请求者事先不知道这个申请最后到底应该由哪个领导处理。
再比如,某个快乐的下午正在快乐次冰棍,你的胃突然有点不舒服,于是决定看看什么情况。首先你去了社区医院,社区医生看了看说可能很严重但也不能确定,你大吃一惊,去了县城的医院。县城的医院做了简单的检查,跟你说可能是胃炎但不确定,建议去更大的医院。然后你来到了省城的医院,医生看了看说,没啥,这就是消化不良(来自在下的亲身经历 )。
和上面请假的例子类似,看病的医院会告诉看病者是否可以治疗,社区医院不成就转院到县城医院,再不行就转院到更大的医院,而看病者一开始在社区医院看病的时候,并不知道这个病最后哪个医院可以治疗,也许是县城医院,也许是省城医院。
在类似的场景中,这些例子有以下特点:
- 请求在一系列对象中传递,形成一条链;
- 链中的请求接受者对请求进行分析,要么处理这个请求,要么把这个请求传递给链的下一个接受者;
2. 实例的代码实现 2.1 代码实现
我们可以使用 JavaScript 来将之前的请假例子实现一下。
var askLeave = function(duration) {
if (duration <= 0.5) {
console.log('小组领导经过一番心理斗争:批准了')
} else if (duration <= 1) {
console.log('部门领导经过一番心理斗争:批准了')
} else if (duration <= 2) {
console.log('总经理经过一番心理斗争:批准了')
} else {
console.log('总经理:不准请这么长的假')
}
}
askLeave(0.5) // 小组领导经过一番心理斗争:批准了
askLeave(1) // 部门领导经过一番心理斗争:批准了
askLeave(2) // 总经理经过一番心理斗争:批准了
askLeave(3) // 总经理:不准请这么长的假
2.2 初步优化
上面的实现没有问题,也可以正常运行,但正常情况下,处理逻辑可能就不仅仅是一个 console.log 这么简单,而是包含一些年假、调休、项目忙碌情况的复杂判断,此时这个 askLeave 方法就变得庞大而臃肿,如果中间增加一个新的领导层,可以批准 1.5 天的假期,那么你就要修改这个庞大的 askLeave 方法,维护工作变得复杂。
这里我们可以将不同领导的处理逻辑(也就是职责节点)提取出来,让不同节点的职责逻辑界限变得明显,代码结构更明显。请假的时候直接找小组领导,如果小组领导处理不好,直接把请求传递给部门领导,部门领导处理不了则传递给总经理。
/* 小组领导处理逻辑 */
var askLeaveGroupLeader = function(duration) {
if (duration <= 0.5) {
console.log('小组领导经过一番心理斗争:批准了')
} else
askLeaveDepartmentLeader(duration)
}
/* 部门领导处理逻辑 */
var askLeaveDepartmentLeader = function(duration) {
if (duration <= 1) {
console.log('部门领导经过一番心理斗争:批准了')
} else
askLeaveGeneralLeader(duration)
}
/* 总经理处理逻辑 */
var askLeaveGeneralLeader = function(duration) {
if (duration <= 2) {
console.log('总经理经过一番心理斗争:批准了')
} else
console.log('总经理:不准请这么长的假')
}
askLeaveGroupLeader(0.5) // 小组领导经过一番心理斗争:批准了
askLeaveGroupLeader(1) // 部门领导经过一番心理斗争:批准了
askLeaveGroupLeader(2) // 总经理经过一番心理斗争:批准了
askLeaveGroupLeader(3) // 总经理:不准请这么长的假
2.3 使用职责链模式重构
上面的实现,逻辑倒是清晰了,也不会有个超大的函数一把梭,但是还有个问题,比如 askLeaveGroupLeader 这个函数里就直接耦合了 askLeaveDepartmentLeader 这个函数,其他函数也是各自耦合在一起,如果要在其中两个职责节点中间增加一个节点,或者去掉一个节点,那么就要同时改动相邻的职责节点函数,这就违反了开闭原则,我们希望增加新的职责节点的时候,对原来的代码没有影响。
这时我们可以引入职责链模式,将职责节点的下一个节点使用拼接的方式,而不是在声明的时候就固定。这里我们:
/* 小组领导 */
var GroupLeader = {
nextLeader: null,
setNext: function(next) {
this.nextLeader = next
},
handle: function(duration) {
if (duration <= 0.5) {
console.log('小组领导经过一番心理斗争:批准了')
} else
this.nextLeader.handle(duration)
}
}
/* 部门领导 */
var DepartmentLeader = {
nextLeader: null,
setNext: function(next) {
this.nextLeader = next
},
handle: function(duration) {
if (duration <= 1) {
console.log('部门领导经过一番心理斗争:批准了')
} else
this.nextLeader.handle(duration)
}
}
/* 总经理 */
var GeneralLeader = {
nextLeader: null,
setNext: function(next) {
this.nextLeader = next
},
handle: function(duration) {
if (duration <= 2) {
console.log('总经理经过一番心理斗争:批准了')
} else
console.log('总经理:不准请这么长的假')
}
}
GroupLeader.setNext(DepartmentLeader) // 设置小组领导的下一个职责节点为部门领导
DepartmentLeader.setNext(GeneralLeader) // 设置部门领导的下一个职责节点为总经理
GroupLeader.handle(0.5) // 小组领导经过一番心理斗争:批准了
GroupLeader.handle(1) // 部门领导经过一番心理斗争:批准了
GroupLeader.handle(2) // 总经理经过一番心理斗争:批准了
GroupLeader.handle(3) // 总经理:不准请这么长的假
这样,将职责的链在使用的时候再拼起来,灵活性好,比如如果要在部门领导和总经理中间增加一个新的职责节点,那么在使用时:
/* 新领导 */
var MewLeader = {
nextLeader: null,
setNext: function(next) {
this.nextLeader = next
},
handle: function(duration) { ... }
}
GroupLeader.setNext(DepartmentLeader) // 设置小组领导的下一个职责节点为部门领导
DepartmentLeader.setNext(MewLeader) // 设置部门领导的下一个职责节点为新领导
MewLeader.setNext(GeneralLeader) // 设置新领导的下一个职责节点为总经理
- 删除节点也是类似操作,非常符合开闭原则了,给维护带来很大方便。
- 但是我们看到之前的内容有很多重复代码,比如
Leader对象里的nextLeader、setNext里的逻辑就是一样的,可以用继承来避免这部分重复。
首先使用 ES5 的方式:
/* 领导基类 */
var Leader = function() {
this.nextLeader = null
}
Leader.prototype.setNext = function(next) {
this.nextLeader = next
}
/* 小组领导 */
var GroupLeader = new Leader()
GroupLeader.handle = function(duration) {
if (duration <= 0.5) {
console.log('小组领导经过一番心理斗争:批准了')
} else
this.nextLeader.handle(duration)
}
/* 部门领导 */
var DepartmentLeader = new Leader()
DepartmentLeader.handle = function(duration) {
if (duration <= 1) {
console.log('部门领导经过一番心理斗争:批准了')
} else
this.nextLeader.handle(duration)
}
/* 总经理 */
var GeneralLeader = new Leader()
GeneralLeader.handle = function(duration) {
if (duration <= 2) {
console.log('总经理经过一番心理斗争:批准了')
} else
console.log('总经理:不准请这么长的假')
}
GroupLeader.setNext(DepartmentLeader) // 设置小组领导的下一个职责节点为部门领导
DepartmentLeader.setNext(GeneralLeader) // 设置部门领导的下一个职责节点为总经理
GroupLeader.handle(0.5) // 小组领导经过一番心理斗争:批准了
GroupLeader.handle(1) // 部门领导经过一番心理斗争:批准了
GroupLeader.handle(2) // 总经理经过一番心理斗争:批准了
GroupLeader.handle(3) // 总经理:不准请这么长的假
我们使用 ES6 的 Class 语法改造一下:
/* 领导基类 */
class Leader {
constructor() {
this.nextLeader = null
}
setNext(next) {
this.nextLeader = next
}
}
/* 小组领导 */
class GroupLeader extends Leader {
handle(duration) {
if (duration <= 0.5) {
console.log('小组领导经过一番心理斗争:批准了')
} else
this.nextLeader.handle(duration)
}
}
/* 部门领导 */
class DepartmentLeader extends Leader {
handle(duration) {
if (duration <= 1) {
console.log('部门领导经过一番心理斗争:批准了')
} else
this.nextLeader.handle(duration)
}
}
/* 总经理 */
class GeneralLeader extends Leader {
handle(duration) {
if (duration <= 2) {
console.log('总经理经过一番心理斗争:批准了')
} else
console.log('总经理:不准请这么长的假')
}
}
const zhangSan = new GroupLeader()
const liSi = new DepartmentLeader()
const wangWu = new GeneralLeader()
zhangSan.setNext(liSi) // 设置小组领导的下一个职责节点为部门领导
liSi.setNext(wangWu) // 设置部门领导的下一个职责节点为总经理
zhangSan.handle(0.5) // 小组领导经过一番心理斗争:批准了
zhangSan.handle(1) // 部门领导经过一番心理斗争:批准了
zhangSan.handle(2) // 总经理经过一番心理斗争:批准了
zhangSan.handle(3) // 总经理:不准请这么长的假
2.4 使用链模式重构
之前的代码实现,我们可以使用链模式稍加重构,在设置下一个职责节点的方法 setNext 中返回下一个节点实例,使得在职责链的组装过程是一个链的形式,代码结构更加简洁。
首先是 ES5 方式:
/* 领导基类 */
var Leader = function() {
this.nextLeader = null
}
Leader.prototype.setNext = function(next) {
this.nextLeader = next
return next
}
/* 小组领导 */
var GroupLeader = new Leader()
GroupLeader.handle = function(duration) { ... }
/* 部门领导 */
var DepartmentLeader = new Leader()
DepartmentLeader.handle = function(duration) { ... }
/* 总经理 */
var GeneralLeader = new Leader()
GeneralLeader.handle = function(duration) { ... }
/* 组装职责链 */
GroupLeader
.setNext(DepartmentLeader) // 设置小组领导的下一个职责节点为部门领导
.setNext(GeneralLeader) // 设置部门领导的下一个职责节点为总经理
ES6 方式同理:
/* 领导基类 */
class Leader {
constructor() {
this.nextLeader = null
}
setNext(next) {
this.nextLeader = next
return next
}
}
/* 小组领导 */
class GroupLeader extends Leader {
handle(duration) { ... }
}
/* 部门领导 */
class DepartmentLeader extends Leader {
handle(duration) { ... }
}
/* 总经理 */
class GeneralLeader extends Leader {
handle(duration) { ... }
}
const zhangSan = new GroupLeader()
const liSi = new DepartmentLeader()
const wangWu = new GeneralLeader()
/* 组装职责链 */
zhangSan
.setNext(liSi) // 设置小组领导的下一个职责节点为部门领导
.setNext(wangWu) // 设置部门领导的下一个职责节点为总经理
3. 职责链模式的原理
职责链模式可能在真实的业务代码中见的不多,但是作用域链、原型链、DOM 事件流的事件冒泡,都有职责链模式的影子:
- 作用域链: 查找变量时,先从当前上下文的变量对象中查找,如果没有找到,就会从父级执行上下文的变量对象中查找,一直找到全局上下文的变量对象。
- 原型链: 当读取实例的属性时,如果找不到,就会查找当前对象关联的原型中的属性,如果还查不到,就去找原型的原型,一直找到最顶层为止。
- 事件冒泡: 事件在 DOM 元素上触发后,会从最内层的元素开始发生,一直向外层元素传播,直到全局 document 对象。
以事件冒泡为例,事件在某元素上触发后,会一级级往外层元素传递事件,如果当前元素没有处理这个事件并阻止冒泡,那么这个事件就会往外层节点传递,就像请求在职责链中的职责节点上传递一样,直到某个元素处理了事件并阻止冒泡。
事件冒泡示意图如下:

可见虽然某些设计模式我们用的不多,但其实已经默默渗入到我们的日常开发中了。
4. 职责链模式的优缺点
职责链模式的优点:
- 和命令模式类似,由于处理请求的职责节点可能是职责链上的任一节点,所以请求的发送者和接受者是解耦的;
- 通过改变链内的节点或调整节点次序,可以动态地修改责任链,符合开闭原则;
职责链模式的缺点:
- 并不能保证请求一定会被处理,有可能到最后一个节点还不能处理;
- 调试不便,调用层次会比较深,也有可能会导致循环引用;
5. 职责链模式的适用场景
- 需要多个对象可以处理同一个请求,具体该请求由哪个对象处理在运行时才确定;
- 在不明确指定接收者的情况下,向多个对象中的其中一个提交请求的话,可以使用职责链模式;
- 如果想要动态指定处理一个请求的对象集合,可以使用职责链模式;
6. 其他相关模式 6.1 职责链模式与组合模式
- 职责链模式可以和组合模式一起使用,比如把职责节点通过组合模式来组合,从而形成组合起来的树状职责链。
6.2 职责链模式与装饰模式
- 这两个模式都是在运行期间动态组合,装饰模式是动态组合装饰器,可以有任意多个对象来装饰功能,而职责链是动态组合职责节点,有一个职责节点处理的话就结束。
- 另外他们的目的也不同,装饰模式为对象添加功能,而职责链模式是要实现发送者和接收者解耦。
中介者模式:找媒人介绍对象
- 中介者模式 (Mediator Pattern)又称调停模式,使得各对象不用显式地相互引用,将对象与对象之间紧密的耦合关系变得松散,从而可以独立地改变他们。核心是多个对象之间复杂交互的封装。
- 根据最少知识原则,一个对象应该尽量少地了解其他对象。如果对象之间耦合性太高,改动一个对象则会影响到很多其他对象,可维护性差。复杂的系统,对象之间的耦合关系会得更加复杂,中介者模式就是为了解决这个问题而诞生的。
1. 你曾见过的中介者模式
举一个有点意思的例子。相亲是一个多方博弈、互相选择(嫌弃)的场景,男生和女生相亲,不仅仅是男生和女生两方关系,还有:
男方的角度:
- 男生会考虑女生的长相、身高、性格、三观、家境、和自己父母能不能合得来、对方家长是否好相处等等;
- 男生的家长会考虑女生的条件、和自己儿子搭不搭、对方家长的性格等等;
女方的角度:
- 女生会考虑男生的性格、上进心、是不是高富帅、能不能通过自己父母的法眼、对方家长是否好相处等等;
- 女生的家长也会考虑男生的条件、是不是配得上自己女儿、对方家长的性格等等;
- 双方的家长可能还有博弈和交换,比如你家是公务员,那么可以稍微穷一点;我家比较帅,那么矮一点对方也可以接受…总之,男生、女生、男方家长、女方家长各方的关系交错复杂,每个人都有自己的考量,如果某一方有什么想法,要和其他三方进行沟通,牵一发动全身。这时候我们可以引入媒人,无论哪一方有什么要求或者什么想法,都可以直接告诉媒人,这样就不用各方自己互相沟通了。
再看另一个例子。比如买房子的时候,我们不必自己去跑到每个卖家那里了解情况,而一般选择从中介那里获取房源信息。卖家们把各自的房源信息提供给中介,包括房源的大小、楼层、朝向等,有的卖家说价格还可以谈,有的卖家说我的房子带阁楼,有的卖家说要跟车库一起卖,等等。买家从中介处就可以获取自己所需的房源信息,比如你不需要车库,也不住一楼和顶楼,只考虑一百平米以上的屋子,中介就会给你筛选出满足你需要的所有房源供你查看,而不需买家一个个的找卖家们了解信息,当你正关注的卖家房子卖出去了中介也会及时告诉你,这就是中介的作用。
类似的例子还有很多,比如电商平台之于买家与店家,聊天平台之于每个聊天者,澳门大型线上赌场之于每个参与赌博的人
在类似的场景中,有以下特点:
- 相亲各方/房源买家卖家(目标对象)之间的关系复杂,引入媒人/中介(中介者)会极大方便各方之间的沟通;
- 相亲各方/房源买家卖家(目标对象)之间如果有什么想法和要求上的变动,通过媒人/中介(中介者)就可以及时通知到相关各方,而目标对象之间相互不通信;
2. 实例的代码实现
我们使用 JavaScript 将刚刚的相亲例子实现一下。
首先我们考虑一个场景,男方和女方都有一定的条件,双方之间有要求,双方家长对对方孩子也有要求,如果达不到要求则不同意这门婚事。(也就是说暂时不考虑男女双方对于对方家长,和双方家长之间的要求,因为这样代码就太长了)
class Person {
/* 个人信息 */
constructor(name, info, target) {
this.name = name
this.info = info // 是一个对象,每一项为数字,比如身高、工资..
this.target = target // 也是对象,每一项为两个数字的数组,表示可接受的最低和最高值
this.enemyList = [] // 考虑列表
}
/* 注册相亲对象及家长 */
registEnemy(...enemy) {
this.enemyList.push(...enemy)
}
/* 检查所有相亲对象及其家长的条件 */
checkAllPurpose() {
this.enemyList.forEach(enemy => enemy.info && this.checkPurpose(enemy))
}
/* 检查对方是否满足自己的要求,并发信息 */
checkPurpose(enemy) {
const result = Object.keys(this.target) // 是否满足自己的要求
.every(key => {
const [low, high] = this.target[key]
return low <= enemy.info[key] && enemy.info[key] <= high
})
enemy.receiveResult(result, this, enemy) // 通知对方
}
/* 接受到对方的信息 */
receiveResult(result, they, me) {
result
? console.log(`${ they.name }:我觉得合适~ \t(我的要求 ${ me.name } 已经满足)`)
: console.log(`${ they.name }:你是个好人! \t(我的要求 ${ me.name } 不能满足!)`)
}
}
/* 男方 */
const ZhangXiaoShuai = new Person(
'张小帅',
{ age: 25, height: 171, salary: 5000 },
{ age: [23, 27] })
/* 男方家长 */
const ZhangXiaoShuaiParent = new Person(
'张小帅家长',
null,
{ height: [160, 167] })
/* 女方 */
const LiXiaoMei = new Person(
'李小美',
{ age: 23, height: 160 },
{ age: [25, 27] })
/* 女方家长 */
const LiXiaoMeiParent = new Person(
'李小美家长',
null,
{ salary: [10000, 20000] })
/* 注册,每一个 person 实例都需要注册对方家庭成员的信息 */
ZhangXiaoShuai.registEnemy(LiXiaoMei, LiXiaoMeiParent)
LiXiaoMei.registEnemy(ZhangXiaoShuai, ZhangXiaoShuaiParent)
ZhangXiaoShuaiParent.registEnemy(LiXiaoMei, LiXiaoMeiParent)
LiXiaoMeiParent.registEnemy(ZhangXiaoShuai, ZhangXiaoShuaiParent)
/* 检查对方是否符合要求,同样,每一个 person 实例都需要执行检查 */
ZhangXiaoShuai.checkAllPurpose()
LiXiaoMei.checkAllPurpose()
ZhangXiaoShuaiParent.checkAllPurpose()
LiXiaoMeiParent.checkAllPurpose()
// 张小帅:我觉得合适~ (我的要求 李小美 已经满足)
// 李小美:我觉得合适~ (我的要求 张小帅 已经满足)
// 张小帅家长:我觉得合适~ (我的要求 李小美 已经满足)
// 李小美家长:你是个好人! (我的要求 张小帅 不能满足!)
当然作为灵活的 JavaScript,并不一定需要使用类,使用对象的形式也是可以的:
const PersonFunc = {
/* 注册相亲对象及家长 */
registEnemy(...enemy) {
this.enemyList.push(...enemy)
},
/* 检查所有相亲对象及其家长的条件 */
checkAllPurpose() {
this.enemyList.forEach(enemy => enemy.info && this.checkPurpose(enemy))
},
/* 检查对方是否满足自己的要求,并发信息 */
checkPurpose(enemy) {
const result = Object.keys(this.target) // 是否满足自己的要求
.every(key => {
const [low, high] = this.target[key]
return low <= enemy.info[key] && enemy.info[key] <= high
})
enemy.receiveResult(result, this, enemy) // 通知对方
},
/* 接受到对方的信息 */
receiveResult(result, they, me) {
result
? console.log(`${ they.name }:我觉得合适~ \t(我的要求 ${ me.name } 已经满足)`)
: console.log(`${ they.name }:你是个好人! \t(我的要求 ${ me.name } 不能满足!)`)
}
}
/* 男方 */
const ZhangXiaoShuai = {
...PersonFunc,
name: '张小帅',
info: { age: 25, height: 171, salary: 5000 },
target: { age: [23, 27] },
enemyList: []
}
/* 男方家长 */
const ZhangXiaoShuaiParent = {
...PersonFunc,
name: '张小帅家长',
info: null,
target: { height: [160, 167] },
enemyList: []
}
/* 女方 */
const LiXiaoMei = {
...PersonFunc,
name: '李小美',
info: { age: 23, height: 160 },
target: { age: [25, 27] },
enemyList: []
}
/* 女方家长 */
const LiXiaoMeiParent = {
...PersonFunc,
name: '李小美家长',
info: null,
target: { salary: [10000, 20000] },
enemyList: []
}
/* 注册 */
ZhangXiaoShuai.registEnemy(LiXiaoMei, LiXiaoMeiParent)
LiXiaoMei.registEnemy(ZhangXiaoShuai, ZhangXiaoShuaiParent)
ZhangXiaoShuaiParent.registEnemy(LiXiaoMei, LiXiaoMeiParent)
LiXiaoMeiParent.registEnemy(ZhangXiaoShuai, ZhangXiaoShuaiParent)
/* 检查对方是否符合要求 */
ZhangXiaoShuai.checkAllPurpose()
LiXiaoMei.checkAllPurpose()
ZhangXiaoShuaiParent.checkAllPurpose()
LiXiaoMeiParent.checkAllPurpose()
// 张小帅:我觉得合适~ (我的要求 李小美 已经满足)
// 李小美:我觉得合适~ (我的要求 张小帅 已经满足)
// 张小帅家长:我觉得合适~ (我的要求 李小美 已经满足)
// 李小美家长:你是个好人! (我的要求 张小帅 不能满足!)
我们还可以使用
Object.create()赋值原型的方式将方法放在原型上,也可以使用原型继承的方式,JavaScript 的灵活性让你可以自由选择习惯的方式。
单就结果而言,上面的代码可以实现整个逻辑。但是这几个对象之间相互引用、相互持有,并紧密耦合。如果继续引入关系,比如张小帅的七大姑、李小美的八大姨,或者考虑的情况更多一些,那么就要改动很多代码,上面的写法就满足不了要求了。
这时我们可以引入媒人(中介者),专门处理对象之间的耦合关系,所有对象间相互不了解,只与媒人交互,如果引入了新的相关方,也只需要通知媒人即可。看一下实现:
``js /* 男方 */ const ZhangXiaoShuai = { name: '张小帅', family: '张小帅家', info: { age: 25, height: 171, salary: 5000 }, target: { age: [23, 27] } }
/* 男方家长 */ const ZhangXiaoShuaiParent = { name: '张小帅家长', family: '张小帅家', info: null, target: { height: [160, 167] } }
/* 女方 */ const LiXiaoMei = { name: '李小美', family: '李小美家', info: { age: 23, height: 160 }, target: { age: [25, 27] } }
/* 女方家长 */ const LiXiaoMeiParent = { name: '李小美家长', family: '李小美家', info: null, target: { salary: [10000, 20000] } }
/* 媒人 */ const MatchMaker = { matchBook: {}, // 媒人的花名册
/* 注册各方 */
registPersons(...personList) {
personList.forEach(person => {
if (this.matchBook[person.family]) {
this.matchBook[person.family].push(person)
} else this.matchBook[person.family] = [person]
})
},
/* 检查对方家庭的孩子对象是否满足要求 */
checkAllPurpose() {
Object.keys(this.matchBook) // 遍历名册中所有家庭
.forEach((familyName, idx, matchList) =>
matchList
.filter(match => match !== familyName) // 对于其中一个家庭,过滤出名册中其他的家庭
.forEach(enemyFamily => this.matchBook[enemyFamily] // 遍历该家庭中注册到名册上的所有成员
.forEach(enemy => this.matchBook[familyName]
.forEach(person => // 逐项比较自己的条件和他们的要求
enemy.info && this.checkPurpose(person, enemy)
)
))
)
},
/* 检查对方是否满足自己的要求,并发信息 */
checkPurpose(person, enemy) {
const result = Object.keys(person.target) // 是否满足自己的要求
.every(key => {
const [low, high] = person.target[key]
return low <= enemy.info[key] && enemy.info[key] <= high
})
this.receiveResult(result, person, enemy) // 通知对方
},
/* 通知对方信息 */
receiveResult(result, person, enemy) {
result
? console.log(`${ person.name } 觉得合适~ \t(${ enemy.name } 已经满足要求)`)
: console.log(`${ person.name } 觉得不合适! \t(${ enemy.name } 不能满足要求!)`)
}
}
/* 注册 */ MatchMaker.registPersons(ZhangXiaoShuai, ZhangXiaoShuaiParent, LiXiaoMei, LiXiaoMeiParent)
MatchMaker.checkAllPurpose()
// 输出: 小帅 觉得合适~ (李小美 已经满足要求) // 输出: 张小帅家长 觉得合适~ (李小美 已经满足要求) // 输出: 李小美 觉得合适~ (张小帅 已经满足要求) // 输出: 李小美家长 觉得不合适! (张小帅 不能满足要求!)
> 可以看到,除了媒人之外,其他各个角色都是独立的,相互不知道对方的存在,对象间关系被解耦,我们甚至可以方便地添加新的对象。比如赵小美家同时还在考虑着孙小拽(emmm…):
```js
// 重写上面「注册」之后的代码
/* 引入孙小拽 */
const SunXiaoZhuai = {
name: '孙小拽',
familyType: '男方',
info: { age: 27, height: 173, salary: 20000 },
target: { age: [23, 27] }
}
/* 孙小拽家长 */
const SunXiaoZhuaiParent = {
name: '孙小拽家长',
familyType: '男方',
info: null,
target: { height: [160, 170] }
}
/* 注册,这里只需要注册一次 */
MatchMaker.registPersons(ZhangXiaoShuai,
ZhangXiaoShuaiParent,
LiXiaoMei,
LiXiaoMeiParent,
SunXiaoZhuai,
SunXiaoZhuaiParent)
/* 检查对方是否符合要求,也只需要检查一次 */
MatchMaker.checkAllPurpose()
// 输出: 张小帅 觉得合适~ (李小美 已经满足要求)
// 输出: 张小帅家长 觉得合适~ (李小美 已经满足要求)
// 输出: 孙小拽 觉得合适~ (李小美 已经满足要求)
// 输出: 孙小拽家长 觉得合适~ (李小美 已经满足要求)
// 输出: 李小美 觉得合适~ (张小帅 已经满足要求)
// 输出: 李小美家长 觉得不合适! (张小帅 不能满足要求!)
// 输出: 李小美 觉得合适~ (孙小拽 已经满足要求)
// 输出: 李小美家长 觉得合适~ (孙小拽 已经满足要求)
从这个例子就已经可以看出中介者模式的优点了,因为各对象之间的相互引用关系被解耦,从而令系统的可扩展性、可维护性更好。
3. 中介者模式的通用实现
对于上面的例子,张小帅、李小美、孙小拽和他们的家长们相当于容易产生耦合的对象(最早的一本设计模式书上将这些对象称为同事,这里也借用一下这个称呼,Colleague),而媒人就相当于中介者(Mediator)。在中介者模式中,同事对象之间互相不通信,而只与中介者通信,同事对象只需知道中介者即可。主要有以下几个概念:
- Colleague: 同事对象,只知道中介者而不知道其他同事对象,通过中介者来与其他同事对象通信;
- Mediator: 中介者,负责与各同事对象的通信;
结构图如下:


可以看到上图,使用中介者模式之后同事对象间的网状结构变成了星型结构,同事对象之间不需要知道彼此,符合最少知识原则。如果同事对象之间需要相互通信,只能通过中介者的方式,这样让同事对象之间原本的强耦合变成弱耦合,强相互依赖变成弱相互依赖,从而让这些同事对象可以独立地改变和复用。原本同事对象间的交互逻辑被中介者封装起来,各个同事对象只需关心自身即可。
4. 中介者模式的优缺点
中介者模式的主要优点有:
- 松散耦合,降低了同事对象之间的相互依赖和耦合,不会像之前那样牵一发动全身;
- 将同事对象间的一对多关联转变为一对一的关联,符合最少知识原则,提高系统的灵活性,使得系统易于维护和扩展;
- 中介者在同事对象间起到了控制和协调的作用,因此可以结合代理模式那样,进行同事对象间的访问控制、功能扩展;
- 因为同事对象间不需要相互引用,因此也可以简化同事对象的设计和实现;
主要缺点是:
- 逻辑过度集中化,当同事对象太多时,中介者的职责将很重,逻辑变得复杂而庞大,以至于难以维护。
当出现中介者可维护性变差的情况时,考虑是否在系统设计上不合理,从而简化系统设计,优化并重构,避免中介者出现职责过重的情况。
5. 中介者模式的适用场景
- 中介者模式适用多个对象间的关系确实已经紧密耦合,且导致扩展、维护产生了困难的场景,也就是当多个对象之间的引用关系变成了网状结构的时候,此时可以考虑使用引入中介者来把网状结构转化为星型结构。
- 但是,如果对象之间的关系耦合并不紧密,或者之间的关系本就一目了然,那么引入中介者模式就是多此一举、画蛇添足。
- 实际上,我们通常使用的
MVC/MVVM框架,就含有中介者模式的思想,Controller/ViewModel层作为中介者协调View/Model进行工作,减少View/Model之间的直接耦合依赖,从而做到视图层和数据层的最大分离。可以关注后面有单独一章分析MVC/MVVM模式,深入了解。
6. 其他相关模式 6.1 中介者模式和外观模式
外观模式和中介者模式思想上有一些相似的地方,但也有不同:
- 中介者模式 将多个平等对象之间内部的复杂交互关系封装起来,主要目的是为了多个对象之间的解耦;
- 外观模式 封装一个子系统内部的模块,是为了向系统外部提供方便的调用;
6.2 中介者模式与发布-订阅模式
- 中介者模式和发布-订阅模式都可以用来进行对象间的解耦,比如发布-订阅模式的发布者/订阅者和中介者模式里面的中介者/同事对象功能上就比较类似。
- 这两个模式也可以组合使用,比如中介者模式就可以使用发布-订阅模式,对相关同事对象进行消息的广播通知。
- 比如上面相亲的例子中,注册各方和通知信息就使用了发布-订阅模式。
6.3 中介者模式与代理模式
同事对象之间需要通信的时候,需要经由中介者,这时中介者就相当于同事对象间的代理。所以这时就可以引入代理模式的概念,对同事对象相互访问的时候,起到访问控制、功能扩展等等功能。
