设计模式之中介者模式
参考资料
- 曾探《JavaScript设计模式与开发实践》;
- 「设计模式 JavaScript 描述」中介者模式
- JavaScript 设计模式之中介者模式
定义
在我们生活的世界中,每个人每个物体之间都会产生一些错综复杂的联系。在应用程序里也是一样,程序由大大小小的单一对象组成,所有这些对象都按照某种关系和规则来通信。
平时我们大概能记住 10 个朋友的电话、30 家餐馆的位置。在程序里,也许一个对象会和其他 10 个对象打交道,所以它会保持 10 个对象的引用。当程序的规模增大,对象会越来越多,它们之间的关系也越来越复杂,难免会形成网状的交叉引用。当我们改变或删除其中一个对象的时候,很可能需要通知所有引用到它的对象。这样一来,就像在心脏旁边拆掉一根毛细血管一般, 即使一点很小的修改也必须小心翼翼,如下图所示。
面向对象设计鼓励将行为分布到各个对象中,把对象划分成更小的粒度,有助于增强对象的可复用性,但由于这些细粒度对象之间的联系激增,又有可能会反过来降低它们的可复用性。
中介者模式的作用就是解除对象与对象之间的紧耦合关系。增加一个中介者对象后,所有的相关对象都通过中介者对象来通信,而不是互相引用,所以当一个对象发生改变时,只需要通知中介者对象即可。中介者使各对象之间耦合松散,而且可以独立地改变它们之间的交互。中介者模式使网状的多对多关系变成了相对简单的一对多关系,如下图所示。
在前面的图中,如果对象 A 发生了改变,则需要同时通知跟 A 发生引用关系的 B、D、E、F 这 4 个对象;而在上图中,使用中介者模式改进之后,A 发生改变时则只需要通知这个中介者对象即可。
使用场景:
- DataX:DataX 是阿里巴巴集团内被广泛使用的离线数据同步工具/平台,实现包括 MySQL、Oracle、SqlServer、Postgre、HDFS、Hive、ADS、HBase、TableStore(OTS)、MaxCompute(ODPS)、DRDS 等各种异构数据源之间高效的数据同步功能。
- 引用关系复杂 : 系统中 对象之间 存在 复杂的 引用关系 , 产生的 相互依赖关系 结构混乱 , 难以理解 ;
- 改变行为 : 交互的 公共行为 , 如果 需要 改变行为 , 可以 增加新的 中介者 类 ; ( 通过增加新的中介者类 , 达到扩展的目的 )
- 多人聊天室 就是一个 中介者模式 场景 , 一个人发言时 , 需要传达给每个人 , 如果没有聊天室 , 需要对每个人都说一遍 , 如果有中介者 , 就由中介者负责将发言传达给每个人 ;
现实中的中介者
1 机场指挥者
中介者也被称为调停者,我们想象一下机场的指挥塔,如果没有指挥塔的存在,每一架飞机要和方圆 100 公里内的所有飞机通信,才能确定航线以及飞行状况,后果是不可想象的。现实中的情况是,每架飞机都只需要和指挥塔通信。指挥塔作为调停者,知道每一架飞机的飞行状况,所以它可以安排所有飞机的起降时间,及时做出航线调整。
2 博彩公司
在世界杯期间购买足球彩票,博彩公司作为中介,每个人只需和博彩公司发生关联,博彩公司会根据所有人的投注情况计算好赔率,彩民们赢了钱就从博彩公司拿,输了钱就交给博彩公司。
中介者模式的例子——泡泡堂游戏
大家可能都还记得泡泡堂游戏,现在我们来一起回顾这个游戏,假设在游戏之初只支持两个玩家同时进行对战。
先定义一个玩家构造函数,它有 3 个简单的原型方法:Play.prototype.win
、Play.prototype.lose
以及表示玩家死亡的 Play.prototype.die
。
因为玩家的数目是 2,所以当其中一个玩家死亡的时候游戏便结束, 同时通知它的对手胜利。 这段代码看起来很简单:
function Player(name) {this.name = namethis.enemy = null; // 敌人
};Player.prototype.win = function () {console.log(this.name + ' won ');
};Player.prototype.lose = function () {console.log(this.name + ' lost');
};Player.prototype.die = function () {this.lose();this.enemy.win();
};
接下来创建 2 个玩家对象:
const player1 = new Player('玩家一');
const player2 = new Player('玩家二');
给玩家相互设置敌人:
player1.enemy = player2;
player2.enemy = player1;
当玩家 player1
被泡泡炸死的时候,只需要调用这一句代码便完成了一局游戏:
javascript
player1.die();// 输出:玩家一 lost、玩家二 won
然而真正的泡泡堂游戏至多可以有 8 个玩家,并分成红蓝两队进行游戏。
为游戏增加队伍
现在我们改进一下游戏。因为玩家数量变多,用下面的方式来设置队友和敌人无疑很低效:
player1.partners = [player1, player2, player3, player4];
player1.enemies = [player5, player6, player7, player8];
Player5.partners = [player5, player6, player7, player8];
Player5.enemies = [player1, player2, player3, player4];
所以我们定义一个数组 players
来保存所有的玩家,在创建玩家之后,循环 players 来给每个玩家设置队友和敌人:
javascript
const players = [];
再改写构造函数 Player
,使每个玩家对象都增加一些属性,分别是队友列表、敌人列表 、 玩家当前状态、角色名字以及玩家所在的队伍颜色:
function Player(name, teamColor) {this.partners = []; // 队友列表this.enemies = []; // 敌人列表this.state = 'live'; // 玩家状态this.name = name; // 角色名字this.teamColor = teamColor; // 队伍颜色
};
玩家胜利和失败之后的展现依然很简单,只是在每个玩家的屏幕上简单地弹出提示:
Player.prototype.win = function () { // 玩家团队胜利console.log('winner: ' + this.name);
};Player.prototype.lose = function () { // 玩家团队失败console.log('loser: ' + this.name);
};
玩家死亡的方法要变得稍微复杂一点,我们需要在每个玩家死亡的时候,都遍历其他队友的生存状况,如果队友全部死亡,则这局游戏失败,同时敌人队伍的所有玩家都取得胜利,代码如下:
Player.prototype.die = function () { // 玩家死亡let all_dead = true;this.state = 'dead'; // 设置玩家状态为死亡for (let i = 0; i < this.partners.length; i++) { // 遍历队友列表if (this.partners[i].state !== 'dead') { // 如果还有一个队友没有死亡,则游戏还未失败all_dead = false;break;}}if (all_dead === true) { // 如果队友全部死亡this.lose(); // 通知自己游戏失败for (let i = 0; i < this.partners.length; i++) { // 通知所有队友玩家游戏失败this.partners[i].lose();}for (let i = 0; i < this.enemies.length; i++) { // 通知所有敌人游戏胜利this.enemies[i].win();}}
};
最后定义一个工厂来创建玩家:
const playerFactory = function (name, teamColor) {const newPlayer = new Player(name, teamColor); // 创建新玩家for (let i = 0; i < players.length; i++) { // 通知所有的玩家,有新角色加入if (players[i].teamColor === newPlayer.teamColor) { // 如果是同一队的玩家players[i].partners.push(newPlayer); // 相互添加到队友列表newPlayer.partners.push(players[i]);} else {players[i].enemies.push(newPlayer); // 相互添加到敌人列表newPlayer.enemies.push(players[i]);}}players.push(newPlayer);return newPlayer;
};
现在来感受一下, 用这段代码创建 8 个玩家:
//红队:
var player1 = playerFactory('皮蛋', 'red'),player2 = playerFactory('小乖', 'red'),player3 = playerFactory('宝宝', 'red'),player4 = playerFactory('小强', 'red');//蓝队:
var player5 = playerFactory('黑妞', 'blue'),player6 = playerFactory('葱头', 'blue'),player7 = playerFactory('胖墩', 'blue'),player8 = playerFactory('海盗', 'blue');
让红队玩家全部死亡:
player1.die();
player2.die();
player4.die();
player3.die();
结果如下:
makefileloser: 宝宝
loser: 皮蛋
loser: 小乖
loser: 小强
winner: 黑妞
winner: 葱头
winner: 胖墩
winner: 海盗
玩家增多带来的困扰
现在我们已经可以随意地为游戏增加玩家或者队伍,但问题是,每个玩家和其他玩家都是紧紧耦合在一起的。在此段代码中,每个玩家对象都有两个属性,this.partners
和 this.enemies
,用来保存其他玩家对象的引用。当每个对象的状态发生改变,比如角色移动、吃到道具或者死亡时,都必须要显式地遍历通知其他对象。
在这个例子中只创建了 8 个玩家,或许还没有对你产生足够多的困扰,而如果在一个大型网络游戏中,画面里有成百上千个玩家,几十支队伍在互相厮杀。如果有一个玩家掉线,必须从所有其他玩家的队友列表和敌人列表中都移除这个玩家。游戏也许还有解除队伍和添加到别的队伍的功能,红色玩家可以突然变成蓝色玩家,这就不再仅仅是循环能够解决的问题了。面对这样的需求,我们上面的代码可以迅速进入投降模式。
用中介者模式改造泡泡堂游戏
现在我们开始用中介者模式来改造上面的泡泡堂游戏, 改造后的玩家对象和中介者的关系如下图所示。
首先仍然是定义 Player
构造函数和 player
对象的原型方法,在 player
对象的这些原型方法 中,不再负责具体的执行逻辑,而是把操作转交给中介者对象,我们把中介者对象命名为 playerDirector
:
function Player(name, teamColor) {this.name = name; // 角色名字this.teamColor = teamColor; // 队伍颜色 this.state = 'alive'; // 玩家生存状态
};Player.prototype.win = function () {console.log(this.name + ' won ');
};Player.prototype.lose = function () {console.log(this.name + ' lost');
};/*******************玩家死亡*****************/Player.prototype.die = function () {this.state = 'dead';playerDirector.reciveMessage('playerDead', this); // 给中介者发送消息,玩家死亡
};/*******************移除玩家*****************/Player.prototype.remove = function () {playerDirector.reciveMessage('removePlayer', this); // 给中介者发送消息,移除一个玩家
};/*******************玩家换队*****************/Player.prototype.changeTeam = function (color) {playerDirector.reciveMessage('changeTeam', this, color); // 给中介者发送消息,玩家换队
};
再继续改写之前创建玩家对象的工厂函数,可以看到,因为工厂函数里不再需要给创建的玩家对象设置队友和敌人,这个工厂函数几乎失去了工厂的意义:
const playerFactory = function (name, teamColor) {const newPlayer = new Player(name, teamColor); // 创造一个新的玩家对象playerDirector.reciveMessage('addPlayer', newPlayer); // 给中介者发送消息,新增玩家return newPlayer;
};
最后,我们需要实现这个中介者 playerDirector
对象,一般有以下两种方式。
- 利用发布—订阅模式。将
playerDirector
实现为订阅者,各player
作为发布者,一旦player
的状态发生改变,便推送消息给playerDirector
,playerDirector
处理消息后将反馈发送 给其他player
。 - 在
playerDirector
中开放一些接收消息的接口,各player
可以直接调用该接口来给playerDirector
发送消息,player
只需传递一个参数给playerDirector
,这个参数的目的是使playerDirector
可以识别发送者。同样,playerDirector
接收到消息之后会将处理结果反馈给其他player
。
这两种方式的实现没什么本质上的区别。在这里我们使用第二种方式,playerDirector
开放一个对外暴露的接口 reciveMessage
,负责接收 player
对象发送的消息,而 player
对象发送消息的时候,总是把自身 this
作为参数发送给 playerDirector
,以便 playerDirector
识别消息来自于哪个玩家对象,代码如下:
const playerDirector = (function () {const players = {}, // 保存所有玩家operations = {}; // 中介者可以执行的操作/*** 新增一个玩家* @param {Player} player 玩家*/operations.addPlayer = function (player) {const teamColor = player.teamColor; // 玩家的队伍颜色// 如果该颜色的玩家还没有成立队伍,则新成立一个队伍players[teamColor] = players[teamColor] || [];players[teamColor].push(player); // 添加玩家进队伍};/*** 移除一个玩家* @param {Player} player 玩家*/operations.removePlayer = function (player) {const teamColor = player.teamColor, // 玩家的队伍颜色teamPlayers = players[teamColor] || []; // 该队伍所有成员for (let i = teamPlayers.length - 1; i >= 0; i--) { // 遍历删除if (teamPlayers[i] === player) {teamPlayers.splice(i, 1);}}};/*** 玩家换队* @param {Player} player 玩家* @param {string} newTeamColor 队伍颜色*/operations.changeTeam = function (player, newTeamColor) { // 玩家换队operations.removePlayer(player); // 从原队伍中删除player.teamColor = newTeamColor; // 改变队伍颜色operations.addPlayer(player); // 增加到新队伍中};/*** 玩家死亡* @param {Player} player 玩家*/operations.playerDead = function (player) {const teamColor = player.teamColor,teamPlayers = players[teamColor]; // 玩家所在队伍let all_dead = true;for (let i = 0; i < teamPlayers.length; i++) {if (teamPlayers[i].state !== 'dead') {all_dead = false;break;}}if (all_dead) { // 全部死亡for (let i = 0; i < teamPlayers.length; i++) {teamPlayers[i].lose(); // 本队所有玩家 lose }for (const color in players) {if (color !== teamColor) {const teamPlayers = players[color]; // 其他队伍的玩家for (let i = 0; i < teamPlayers.length; i++) {teamPlayers[i].win(); // 其他队伍所有玩家 win }}}}};const reciveMessage = function () {// arguments 的第一个参数为消息名称const message = Array.prototype.shift.call(arguments); operations[message].apply(this, arguments);};return {reciveMessage}})();
可以看到,除了中介者本身,没有一个玩家知道其他任何玩家的存在,玩家与玩家之间的耦合关系已经完全解除,某个玩家的任何操作都不需要通知其他玩家,而只需要给中介者发送一个消息,中介者处理完消息之后会把处理结果反馈给其他的玩家对象。我们还可以继续给中介者扩展更多功能,以适应游戏需求的不断变化。
我们来看下测试结果:
// 红队:
var player1 = playerFactory('皮蛋', 'red'),player2 = playerFactory('小乖', 'red'),player3 = playerFactory('宝宝', 'red'),player4 = playerFactory('小强', 'red');// 蓝队:
var player5 = playerFactory('黑妞', 'blue'),player6 = playerFactory('葱头', 'blue'),player7 = playerFactory('胖墩', 'blue'),player8 = playerFactory('海盗', 'blue');player1.die();
player2.die();
player3.die();
player4.die();
运行结果如下。
皮蛋 lost
小乖 lost
宝宝 lost
小强 lost
黑妞 won
葱头 won
胖墩 won
海盗 won
假设皮蛋和小乖掉线
player1.remove();
player2.remove();
player3.die();
player4.die();
则结果如下。
宝宝 lost
小强 lost
黑妞 won
葱头 won
胖墩 won
海盗 won
假设皮蛋从红队叛变到蓝队
player1.changeTeam( 'blue' );
player2.die();
player3.die();
player4.die();
则结果如下。
小乖 lost
宝宝 lost
小强 lost
黑妞 won
葱头 won
胖墩 won
海盗 won
皮蛋 won
中介者模式的例子——购买商品
需求:实现购买手机的页面,在购买流程中,可以选择手机的颜色以及输入购买数量,同时页面中有两个展示区域,分别向用户展示刚刚选择好的颜色和数量。还有一个按钮动态显示下一步的操作,我们需要查询该颜色手机对应的库存,如果库存数量少于这次的购买数量,按钮将被禁用并且显示库存不足,反之按钮可以点击并且显示放入购物车。
假设手机库存为:
var goods = {"red": 3,"blue": 6
}
那么页面中,会有一下几种场景:
- 选择红色手机,购买 4 个,库存不足。
- 选择蓝色手机,购买 5 个,库存充足,可以加入购物车。
- 没有输入购买数量的时候,按钮将被禁用并显示相应提示。
那么基本上至少有5个节点:
- 下拉选择框
colorSelect
- 文本输入框
numberInput
- 展示颜色信息
colorInfo
- 展示购买数量信息
numberInfo
- 决定下一步操作的按钮
nextBtn
开始编码
HTML代码:
选择颜色:
<select id="colorSelect"><option value="">请选择</option><option value="red">红色</option><option value="blue">蓝色</option>
</select>
输入购买数量: <input type="text" id="numberInput" />
您选择了颜色: <div id="colorInfo"></div>
您输入了数量: <div id="numberInfo"></div>
<button id="nextBtn" disabled="true">请选择手机颜色和购买数量</button>
接下来将分别监听 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 - 0) | 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
之后,会发生什么事情。首先我们要让 colorInfo
中显示当前选中的颜色,然后获取用户当前输入的购买数量,对用户的输入值进行一些合法性判断。再根据库存数量来判断 nextBtn
的显示状态。
numberInput.oninput = function () {var color = colorSelect.value, // 颜色number = this.value, // 数量stock = goods[color]; // 该颜色手机对应的当前库存numberInfo.innerHTML = number;if (!color) {nextBtn.disabled = true;nextBtn.innerHTML = '请选择手机颜色';return;}if (((number - 0) | 0) !== number - 0) {nextBtn.disabled = true;nextBtn.innerHTML = '请输入正确的购买数量';return;}if (number > stock) { // 当前选择数量没有超过库存量 nextBtn.disabled = true;nextBtn.innerHTML = '库存不足';return;}nextBtn.disabled = false;nextBtn.innerHTML = '放入购物车';
};
可能遇到的困难
虽然目前顺利完成了代码编写,但随之而来的需求改变有可能给我们带来麻烦。假设现在要求去掉 colorInfo
和 numberInfo
这两个展示区域,我们就要分别改动 colorSelect.onchange
和 numberInput.onput
里面的代码,因为在先前的代码中,这些对象确实是耦合在一起的。
那么现在,我们页面中需要增加另一个下拉选择框,代表选择手机内存,我们需要计算颜色、内存和购买数量来判断 nextBtn
是显示库存不足还是放入购物车。
首先要增加两个 HTML
节点:
选择内存:
<select id="memorySelect"><option value="">请选择</option><option value="32G">32G</option><option value="16G">16G</option>
</select>
您选择了内存: <div id="memoryInfo"></div>
<script>
memorySelect = document.getElementById('memorySelect'),
memoryInfo = document.getElementById('memoryInfo')
</script>
接下来修改表示存库的 JSON
对象以及修改 colorSelect
的 onchange
事件函数:
var goods = { // 手机库存"red|32G": 3, // 红色 32G,库存数量为 3 "red|16G": 0,"blue|32G": 1,"blue|16G": 6
};
colorSelect.onchange = function () {
/// 除上述代码外,还有以下判断var color = this.value, // 颜色number = numberInput.value,stock = goods[color + '|' + memory]; // 该颜色手机对应的当前库存if (!memory) {nextBtn.disabled = true;nextBtn.innerHTML = '请选择内存大小';return;}
}
同样要改些 numberInput
事件的相关代码。
最后还要新增 memorySelect
的 onchange
事件函数:
memorySelect.onchange = function () {var color = colorSelect.value,number = numberInput.value,memory = this.value,stock = goods[color + '|' + memory];if (!color) {nextBtn.disabled = true;nextBtn.innerHTML = '请选择手机颜色';return;}if (!memory) {nextBtn.disabled = true;nextBtn.innerHTML = '请选择内存大小';return;}if (((number - 0) | 0) !== number - 0) {nextBtn.disabled = true;nextBtn.innerHTML = '请输入正确的购买数量';return;}if (number > stock) { // 当前选择数量没有超过库存量nextBtn.disabled = true;nextBtn.innerHTML = '库存不足';return;}nextBtn.disabled = false;nextBtn.innerHTML = '放入购物车';
}
我们可以看到,仅仅增加了一个内存的选择条件,就需要修改如此多的代码,这是因为在目前的实现中,每个节点对象都是耦合在一起的,改变或者增加任何一个节点对象,都要通知到与其相关的对象。
引入中介者
现在引入中介者对象,所有的节点对象只跟中介者通信。当下拉选择框 colorSelect
、memorySelect
和文本输入框 numberInput
发生了事件行为时,它们仅仅通知中介者它们被改变了,同时把自身当作参数传入中介者,以便中介者辨别是谁发生了改变。剩下的所有事情都交给中介者对象来完成。
var goods = { // 手机库存 "red|32G": 3,"red|16G": 0,"blue|32G": 1,"blue|16G": 6
};
var mediator = (function () {var colorSelect = document.getElementById('colorSelect'),memorySelect = document.getElementById('memorySelect'),numberInput = document.getElementById('numberInput'),colorInfo = document.getElementById('colorInfo'),memoryInfo = document.getElementById('memoryInfo'),numberInfo = document.getElementById('numberInfo'),nextBtn = document.getElementById('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 (!color) {nextBtn.disabled = true;nextBtn.innerHTML = '请选择手机颜色';return;}if (!memory) {nextBtn.disabled = true;nextBtn.innerHTML = '请选择内存大小';return;}if (((number - 0) | 0) !== number - 0) {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|32G|800": 3, // 颜色 red,内存 32G,cpu800,对应库存数量为 3"red|16G|801": 0,"blue|32G|800": 1,"blue|16G|801": 6
};
var mediator = (function () {var cpuSelect = document.getElementById('cpuSelect');return {change: function (obj) {// 略var cpu = cpuSelect.value,stock = goods[color + '|' + memory + '|' + cpu];}}// 略if (obj === cpuSelect) {cpuInfo.innerHTML = cpu;}
})();
优缺点
优点:
- 中介者模式使各个对象之间得以解耦,以中介者和对象之间的一对多关系取代了对象之间的网状多对多关系。各个对象只需关注自身功能的实现,对象之间的交互关系交给了中介者对象来实现和维护。
缺点:
- 最大的缺点是系统中会新增一个中介者对象,因为对象之间交互的复杂性,转移成了中介者对象的复杂性,使得中介者对象经常是巨大的。中介者对象自身往往就是一个难以维护的对象。
总结
中介者模式是迎合迪米特法则的一种实现。迪米特法则也叫最少知识原则,是指一个对象应该尽可能少地了解另外的对象(类似不和陌生人说话)。如果对象之间的耦合性太高,一个对象发生改变之后,难免会影响到其他的对象,跟“城门失火,殃及池鱼”的道理是一样的。而在中介者模式里,对象之间几乎不知道彼此的存在,它们只能通过中介者对象来互相影响对方。
中介者模式可以非常方便地对模块或者对象进行解耦,但对象之间并非一定需要解耦。在实际项目中,模块或对象之间有一些依赖关系是很正常的。毕竟我们写程序是为了快速完成项目交付生产,而不是堆砌模式和过度设计。关键就在于如何去衡量对象之间的耦合程度。一般来说, 如果对象之间的复杂耦合确实导致调用和维护出现了困难,而且这些耦合度随项目的变化呈指数增长曲线,那我们就可以考虑用中介者模式来重构代码。
相关文章:

设计模式之中介者模式
参考资料 曾探《JavaScript设计模式与开发实践》;「设计模式 JavaScript 描述」中介者模式JavaScript 设计模式之中介者模式 定义 在我们生活的世界中,每个人每个物体之间都会产生一些错综复杂的联系。在应用程序里也是一样,程序由大大小小…...

DJ5-8 磁盘存储器的性能和调度
目录 5.8.1 磁盘性能简述 1、磁盘的结构和布局 2、磁盘的类型 3、磁盘数据的组织和格式 4、磁盘的访问过程 5、磁盘访问时间 5.8.2 磁盘调度算法 1、先来先服务 FCFS 2、最短寻道时间优先 SSTF 3、扫描算法(电梯算法)SCAN 4、循环扫描算法 …...

springboot+vue留守儿童爱心网站(源码+文档)
风定落花生,歌声逐流水,大家好我是风歌,混迹在java圈的辛苦码农。今天要和大家聊的是一款基于springboot的留守儿童爱心网站。项目源码以及部署相关请联系风歌,文末附上联系信息 。 💕💕作者:风…...

数字设计小思 - 谈谈非理想时钟的时钟偏差
写在前面 本系列整理数字系统设计的相关知识体系架构,为了方便后续自己查阅与求职准备。在FPGA和ASIC设计中,时钟信号的好坏很大程度上影响了整个系统的稳定性,本文主要介绍了数字设计中的非理想时钟的偏差来源与影响。 (本文长…...

智慧厕所引导系统的应用
智慧公厕引导系统是一种基于智能化技术的公厕管理系统,可以为如厕者提供更加便捷、舒适、安全的如厕环境和服务,同时也可以引导如厕者文明如厕,营造文明公厕的氛围。智慧公厕引导系统可以通过智能引导屏、手机小程序等方式,为如厕…...

眼球追踪、HDR、VST,从代码挖掘Valve下一代VR头显
擅长爆料、挖掘线索的Brad Lynch,此前发布了Quest Pro等设备的线索文章引发关注。近期,又公布一系列与“Valve Deckard”VR头显相关消息,比如支持眼球追踪、HDR、VST透视、Wi-Fi网络等等。在SteamVR 1.26.1测试版更新、Steam用户端、Gamesc…...

【MYSQL】聚合函数和单表/多表查询练习、子查询、内外连接
目录 1.聚合函数 1.1.group by子句 1.2.having语句 2.单表查询 2.2单表查询 3.多表查询 3.2.子查询 5.内链接 6.外连接 1.聚合函数 函数说明count返回查询到的数据的数量sum返回查询到的数据的总和avg返回查询到的数据的平均值max返回查询到的数据的最大值min返回查询…...

分布式数据库集成解决方案
分布式数据库集成解决方案 分析访问部署扩展.1 以界面方式创建数据库(采用DBCA) # 背景 由于公司业务的发展,要求在其它三个城市设立货仓,处理发货业务。公司本部运行着一套用Sybase数据库的MIS系统可以实现发货,该系统…...

如何配置静态路由?这个实例详解交换机的静态路由配置
一、什么是静态路由 静态路由是一种路由的方式,它需要通过手动配置。静态路由与动态路由不同,静态路由是固定的,不会改变。一般来说,静态路由是由网络管理员逐项加入路由表,简单来说,就是需要手动添加的。…...
OpenCV教程——图像操作。读写像素值,与/或/非/异或操作,ROI
1.读取像素值 我们可以通过mat.ptr<uchar>()获取图像某一行像素数组的指针。因此如果想要读取点(x50,y0)(⚠️即(row0,col50))的像素值,可以这样做:mat.ptr<uchar>(0)[50]。 在本节将介绍另外几种直接读…...

Winforms不可见组件开发
Winforms不可见组件开发 首先介绍基本知识,有很多的朋友搞不清楚Component与Control之间的区别,比较简单形象的区别有下面两点: 1、Component在运行时不能呈现UI,而Control可以在运行时呈现UI。 2、Component是贴在容器Container上的,而Control则是贴…...

静态链接库与动态链接库
静态链接库与动态链接库 一、从源程序到可执行文件二、编译、链接和装入三、静态链接库与动态链接库四、静态链接库与动态链接库的制作与使用1.静态库的制作及使用2.动态库的制作及使用 一、从源程序到可执行文件 由于计算机无法直接理解和执行高级语言(C、C、Java…...
ffmpeg 抓取一帧数据
FFmpeg功能比较强大,这里记录一条从摄像机抓拍的一条命令: ffmpeg.exe -i rtsp://admin:hisense2021192.168.1.64:554/live0.264 -r 1 -ss 00:00:00 -t 00:00:01 -f image2 image.jpg ; ---执行成功。 这是一条网络摄像机的抓图命令,其实就…...

学好数据结构的秘诀
学好数据结构的秘诀 作为计算机专业的一名“老兵”,笔者从事数据结构和算法的研究已经近20余年了,在学习的过程中,也会遇到一些问题,但在解决问题时,积累了一些经验,为了让读者在学习数据结构的过程中少走…...

IT知识百科:什么是下一代防火墙和IPS?
引言 随着网络攻击的日益增多,防火墙和入侵防御系统(Intrusion Prevention System, IPS)已成为企业网络安全的必备设备。然而,传统的防火墙和IPS已经无法满足复杂多变的网络安全威胁,因此,下一代防火墙和I…...
常量指针和指针常量, top-level const和low-level const
区分常量指针和指针常量,并且认识什么是top-level const和low-level const。 1.判别: 拿到一个指针(例如const int* a),就从左往右读,只看const和*。const读作常量,*读作指针,int类型这些不用管。 2.指针常量 int a…...

【iOS】-- GET和POST(NSURLSession)
文章目录 NSURLSessionGET和POST区别 GET方法GET请求步骤 POSTPOST请求步骤 NSURLSessionDataDelegate代理方法AFNetWorking添加头文件GETPOST第一种第二种 NSURLSession 使用NSURLSession,一般有两步操作:通过NSURLSession的实例创建task;执…...

@RequestBody,@RequestParam,@RequestPart应用场景和区别
ReqeustBody 使用此注解接收参数时,适用于请求体格式为 application/json,只能用对象接收 RequestParam 支持application/json,也同样支持multipart/form-data请求 RequestPart RequestPart这个注解用在multipart/form-data表单提交请求的方法…...

libevent高并发网络编程 - 02_libevent缓冲IO之bufferevent
文章目录 1. 为什么需要缓冲区?2. 水位3. bufferevent常用API3.1 evconnlistener_new_bind()3.2 evconnlistener_free()3.3 bufferevent_socket_new()3.4 bufferevent_enable()3.5 bufferevent_set_timeouts()3.6 bufferevent_setcb()3.7 bufferevent_setwatermark(…...

院内导航移动导诊服务体系,院内导航怎么实现?
院内导航怎么实现?经过多年发展,医院规模愈加庞大,尤其是综合性医院,院区面积较大,门诊、医技、住院等大楼及楼区内部设计复杂,科室、诊室数量众多,对于新患者犹如进入了迷宫,客观环…...

(LeetCode 每日一题) 3442. 奇偶频次间的最大差值 I (哈希、字符串)
题目:3442. 奇偶频次间的最大差值 I 思路 :哈希,时间复杂度0(n)。 用哈希表来记录每个字符串中字符的分布情况,哈希表这里用数组即可实现。 C版本: class Solution { public:int maxDifference(string s) {int a[26]…...

龙虎榜——20250610
上证指数放量收阴线,个股多数下跌,盘中受消息影响大幅波动。 深证指数放量收阴线形成顶分型,指数短线有调整的需求,大概需要一两天。 2025年6月10日龙虎榜行业方向分析 1. 金融科技 代表标的:御银股份、雄帝科技 驱动…...

shell脚本--常见案例
1、自动备份文件或目录 2、批量重命名文件 3、查找并删除指定名称的文件: 4、批量删除文件 5、查找并替换文件内容 6、批量创建文件 7、创建文件夹并移动文件 8、在文件夹中查找文件...

dedecms 织梦自定义表单留言增加ajax验证码功能
增加ajax功能模块,用户不点击提交按钮,只要输入框失去焦点,就会提前提示验证码是否正确。 一,模板上增加验证码 <input name"vdcode"id"vdcode" placeholder"请输入验证码" type"text&quo…...

Psychopy音频的使用
Psychopy音频的使用 本文主要解决以下问题: 指定音频引擎与设备;播放音频文件 本文所使用的环境: Python3.10 numpy2.2.6 psychopy2025.1.1 psychtoolbox3.0.19.14 一、音频配置 Psychopy文档链接为Sound - for audio playback — Psy…...

NLP学习路线图(二十三):长短期记忆网络(LSTM)
在自然语言处理(NLP)领域,我们时刻面临着处理序列数据的核心挑战。无论是理解句子的结构、分析文本的情感,还是实现语言的翻译,都需要模型能够捕捉词语之间依时序产生的复杂依赖关系。传统的神经网络结构在处理这种序列依赖时显得力不从心,而循环神经网络(RNN) 曾被视为…...
Java编程之桥接模式
定义 桥接模式(Bridge Pattern)属于结构型设计模式,它的核心意图是将抽象部分与实现部分分离,使它们可以独立地变化。这种模式通过组合关系来替代继承关系,从而降低了抽象和实现这两个可变维度之间的耦合度。 用例子…...

20个超级好用的 CSS 动画库
分享 20 个最佳 CSS 动画库。 它们中的大多数将生成纯 CSS 代码,而不需要任何外部库。 1.Animate.css 一个开箱即用型的跨浏览器动画库,可供你在项目中使用。 2.Magic Animations CSS3 一组简单的动画,可以包含在你的网页或应用项目中。 3.An…...

【JVM】Java虚拟机(二)——垃圾回收
目录 一、如何判断对象可以回收 (一)引用计数法 (二)可达性分析算法 二、垃圾回收算法 (一)标记清除 (二)标记整理 (三)复制 (四ÿ…...

宇树科技,改名了!
提到国内具身智能和机器人领域的代表企业,那宇树科技(Unitree)必须名列其榜。 最近,宇树科技的一项新变动消息在业界引发了不少关注和讨论,即: 宇树向其合作伙伴发布了一封公司名称变更函称,因…...