中介者模式

[TOC]

原理

示例

泡泡堂(js实现)

2人版本

游戏之初只支持两个玩家同时进行对战

先定义一个玩家构造函数,它有3 个简单的原型方法:Play.prototype.winPlay.prototype.lose以及表示玩家死亡的Play.prototype.die

当其中一个玩家死亡的时候游戏便结束, 同时通知它的对手胜利

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
function Player(name) {
this.name = name
this.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 个玩家对象:
var player1 = new Player('皮蛋');
var player2 = new Player('小乖');
//给玩家相互设置敌人:
player1.enemy = player2;
player2.enemy = player1;

player1.die();// 输出:皮蛋 lost、小乖 won

真正的泡泡堂游戏至多可以有8 个玩家,并分成红蓝两队进行游戏。

为游戏增加队伍

现在我们改进一下游戏。因为玩家数量变多,用下面的方式来设置队友和敌人无疑很低效:

1
2
3
4
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 来给每个玩家设置队友和敌人

再改写构造函数Player,使每个玩家对象都增加一些属性,分别是队友列表、敌人列表 、玩家当前状态、角色名字以及玩家所在的队伍颜色:

1
2
3
4
5
6
7
8
var players = [];
function Player( name, teamColor ){
this.partners = []; // 队友列表
this.enemies = []; // 敌人列表
this.state = 'live'; // 玩家状态
this.name = name; // 角色名字
this.teamColor = teamColor; // 队伍颜色
};

玩家胜利和失败之后的展现依然很简单,只是在每个玩家的屏幕上简单地弹出提示:

1
2
3
4
5
6
Player.prototype.win = function(){ // 玩家团队胜利
console.log( 'winner: ' + this.name );
};
Player.prototype.lose = function(){ // 玩家团队失败
console.log( 'loser: ' + this.name );
};

玩家死亡的方法要变得稍微复杂一点,我们需要在每个玩家死亡的时候,都遍历其他队友的生存状况,如果队友全部死亡,则这局游戏失败,同时敌人队伍的所有玩家都取得胜利,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
Player.prototype.die = function () { // 玩家死亡
var all_dead = true;

this.state = 'dead'; // 设置玩家状态为死亡
for (var i = 0, partner; partner = this.partners[i++];) { // 遍历队友列表
if (partner.state !== 'dead') { // 如果还有一个队友没有死亡,则游戏还未失败
all_dead = false;
break;
}
}
if (all_dead === true) { // 如果队友全部死亡
this.lose(); // 通知自己游戏失败
for (var i = 0, partner; partner = this.partners[i++];) { // 通知所有队友玩家游戏失败
partner.lose();
}
for (var i = 0, enemy; enemy = this.enemies[i++];) { // 通知所有敌人游戏胜利
enemy.win();
}
}
};

最后定义一个工厂来创建玩家:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
var playerFactory = function (name, teamColor) {
var newPlayer = new Player(name, teamColor); // 创建新玩家
for (var i = 0, player; player = players[i++];) { // 通知所有的玩家,有新角色加入
if (player.teamColor === newPlayer.teamColor) { // 如果是同一队的玩家
player.partners.push(newPlayer); // 相互添加到队友列表
newPlayer.partners.push(player);
} else {
player.enemies.push(newPlayer); // 相互添加到敌人列表
newPlayer.enemies.push(player);
}
}
players.push(newPlayer);
return newPlayer;
};

用这段代码创建8 个玩家:

1
2
3
4
5
6
7
8
9
10
//红队:
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');

让红队玩家全部死亡:

1
2
3
4
player1.die();
player2.die();
player4.die();
player3.die();

执行结果如图

玩家增多带来的困扰

现在我们已经可以随意地为游戏增加玩家或者队伍,但问题是,每个玩家和其他玩家都是紧紧耦合在一起的。在此段代码中,每个玩家对象都有两个属性,this.partners 和this.enemies,用来保存其他玩家对象的引用。当每个对象的状态发生改变,比如角色移动、吃到道具或者死亡时,都必须要显式地遍历通知其他对象。

如果在一个大型网络游戏中,画面里有成百上千个玩家,几十支队伍在互相厮杀。如果有一个玩家掉线,必须从所
有其他玩家的队友列表和敌人列表中都移除这个玩家。游戏也许还有解除队伍和添加到别的队伍的功能,红色玩家可以突然变成蓝色玩家,这就不再仅仅是循环能够解决的问题了。

用中介者模式改造泡泡堂游戏

现在我们开始用中介者模式来改造上面的泡泡堂游戏, 改造后的玩家对象和中介者的关系
如图 所示:

首先仍然是定义Player 构造函数和player 对象的原型方法,在player 对象的这些原型方法中,不再负责具体的执行逻辑,而是把操作转交给中介者对象,我们把中介者对象命名为playerDirector:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
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); // 给中介者发送消息,玩家换队
};

再继续改写之前创建玩家对象的工厂函数,可以看到,因为工厂函数里不再需要给创建的玩家对象设置队友和敌人,这个工厂函数几乎失去了工厂的意义:

1
2
3
4
5
var playerFactory = function (name, teamColor) {
var 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 识别消息来自于哪个玩家对象,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
var playerDirector = (function () {
var players = {}, // 保存所有玩家
operations = {}; // 中介者可以执行的操作
/****************新增一个玩家***************************/
operations.addPlayer = function (player) {
var teamColor = player.teamColor; // 玩家的队伍颜色
players[teamColor] = players[teamColor] || []; // 如果该颜色的玩家还没有成立队伍,则

players[teamColor].push(player); // 添加玩家进队伍
};
/****************移除一个玩家***************************/
operations.removePlayer = function (player) {
var teamColor = player.teamColor, // 玩家的队伍颜色
teamPlayers = players[teamColor] || []; // 该队伍所有成员
for (var i = teamPlayers.length - 1; i >= 0; i--) { // 遍历删除
if (teamPlayers[i] === player) {
teamPlayers.splice(i, 1);
}
}
};
/****************玩家换队***************************/
operations.changeTeam = function (player, newTeamColor) { // 玩家换队
operations.removePlayer(player); // 从原队伍中删除
player.teamColor = newTeamColor; // 改变队伍颜色
operations.addPlayer(player); // 增加到新队伍中
};

operations.playerDead = function (player) { // 玩家死亡
var teamColor = player.teamColor,
teamPlayers = players[teamColor]; // 玩家所在队伍
var all_dead = true;
for (var i = 0, player; player = teamPlayers[i++];) {
if (player.state !== 'dead') {
all_dead = false;
break;
}
}

if (all_dead === true) { // 全部死亡
teamPlayers.forEach(player => { // 本队所有玩家lose
player.lose();
});

for (var color in players) {
if (color !== teamColor) {
players[color].forEach(player => { // 其他队伍所有玩家win
player.win();
});
}
}
}
};

var reciveMessage = function () {
var message = Array.prototype.shift.call(arguments); // arguments 的第一个参数为消息名称
// arguments删除掉第一个参数剩余的传给实际调用的方法
operations[message].apply(this, arguments);
};

return { reciveMessage }
})();

可以看到,除了中介者本身,没有一个玩家知道其他任何玩家的存在,玩家与玩家之间的耦合关系已经完全解除,某个玩家的任何操作都不需要通知其他玩家,而只需要给中介者发送一个消息,中介者处理完消息之后会把处理结果反馈给其他的玩家对象。我们还可以继续给中介者扩展更多功能,以适应游戏需求的不断变化。

测试

注意每次测试重置状态

  • 测试1:红方全部死亡

    1
    2
    3
    4
    player1.die();
    player2.die();
    player3.die();
    player4.die();

  • 测试2:皮蛋和小乖掉线

    1
    2
    3
    4
    player1.remove();
    player2.remove();
    player3.die();
    player4.die();

  • 测试3:皮蛋从红队叛变到蓝队

    1
    2
    3
    4
    player1.changeTeam('blue');
    player2.die();
    player3.die();
    player4.die();

找房子(python实现)

人在江湖漂,岂能顺心如意?与大多数毕业生一样,第一份工作很难持续两年以上。Tony 也在一家公司工作了一年半后,换了一个东家。

在北京这个硕大的城市里,换工作基本就意味着要换房子。不得不说,找房子是一件烦心而累人的工作。

  1. 你首先要知道自己要怎样的房子:多大面积(多少平米),什么价位,是否有窗户,是否有独卫。
  2. 要去网上查找各种房源信息,找到最匹配的几个户型。
  3. 之后要去电话咨询,过滤虚假信息和过时信息。
  4. 最后,也是最累人的一步,要去实地考查,看看真实的房子与网上的信息是否相符,房间是否有异味,周围设施是否齐全。这一步你可能会从东城穿越西城,再来到南城,而后又折腾去北城……想想都累!
  5. 最后的最后,你还要与各种脾性的房东进行周旋,去讨价还价。

Tony 想了想,还是找中介算了。在北京这座城市,你几乎找不到一手房东,90%的房源信息都掌握在房屋中介手中!既然都找不到一手房东,还不如找一家正规点的中介。

于是 Tony 找到了我爱我家,认识了里面的职员 Vangie。Vangie 问了他对房子的要求。Tony 说:“18平米左右,要有独卫,要有窗户,最好是朝南,有厨房更好!价位在2000左右。”Vangie 立马就说:“上地西里有一间,但没有厨房;当代城市家园有两间,一间主卧,一间次卧,但卫生间是共用的;美和园有一间,比较适合你,但价格会贵一点。” 真是了如指掌啊!说完就带着 Tony 开始看房了……

一天就找到了还算合适的房子。但不得不再次吐槽:北京的房子真 TM 贵啊,18平米,精装修,有朝南窗户,一个超小(1m宽不到)的阳台,卫生间5人共用,厨房共用,价格要2600每月。押一付三,加一个月的中介费,一次交了一万多,要开始吃土了,内心滴了无数滴血……

上面的生活场景中,Tony 通过中介来找房子,因为找房子的过程实在太繁琐了,而且对房源信息不了解。通过中介,他省去了很多麻烦的细节,合同也是直接跟中介签,你甚至可能都不知道房东是谁。

我们将通过程序来模拟一下上面找房子的过程。

源码示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
class HouseInfo:
"房源信息"

def __init__(self, area, price, hasWindow, bathroom, kitchen, address,
owner):
self.__area = area
self.__price = price
self.__window = hasWindow
self.__bathroom = bathroom
self.__kitchen = kitchen
self.__address = address
self.__owner = owner

def getAddress(self):
return self.__address

def getOwnerName(self):
return self.__owner.getName()

def showInfo(self, isShowOwner=True):
print("面积:" + str(self.__area) + "平米", "价格:" + str(self.__price) + "元",
"窗户:" + ("有" if self.__window else "没有"),
"卫生间:" + self.__bathroom,
"厨房:" + ("有" if self.__kitchen else "没有"),
"地址:" + self.getAddress(),
"房东:" + self.getOwnerName() if isShowOwner else "")


class HousingAgency:
"房屋中介"

def __init__(self, name):
self.__houseInfos = []
self.__name = name

def getName(self):
return self.__name

def addHouseInfo(self, houseInfo):
self.__houseInfos.append(houseInfo)

def removeHouseInfo(self, houseInfo):
for info in self.__houseInfos:
if (info == houseInfo):
self.__houseInfos.remove(info)

def getSearchCondition(self, description):
"这里有一个将用户描述信息转换成搜索条件的逻辑。(为节省篇幅这里原样返回描述)"
return description

def getMatchInfos(self, searchCondition):
"根据房源信息的各个属性查找最匹配的信息。(为节省篇幅这里略去匹配的过程,全部输出)"
print(self.getName(), "为您找以下最适合的房源:")
for info in self.__houseInfos:
info.showInfo(False)
return self.__houseInfos

def signContract(self, houseInfo, time):
"与房东签订协议"
print(self.getName(), "与房东", houseInfo.getOwnerName(), "签订",
houseInfo.getAddress(), "的房子的的租赁合同,租期", time, "年。 合同期内",
self.getName(), "有权对其进行使用和转租!")

def signContracts(self, time):
for info in self.__houseInfos:
self.signContract(info, time)


class HouseOwner:
"房东"

def __init__(self, name, address):
self.__name = name
self.__address = address
self.__houseInfo = None

def getName(self):
return self.__name

def getAddress(self):
return self.__address

def setHouseInfo(self, area, price, hasWindow, bathroom, kitchen):
self.__houseInfo = HouseInfo(area, price, hasWindow, bathroom, kitchen,
self.getAddress(), self)

def publishHouseInfo(self, agency):
agency.addHouseInfo(self.__houseInfo)
print(self.getName() + "在", agency.getName(), "发布房源出租信息:")
self.__houseInfo.showInfo()


class Custom:
"房客,租房人"

def __init__(self, name):
self.__name = name

def getName(self):
return self.__name

def findHouse(self, description, agency):
print("我是" + self.getName() + ", 我想要找一个\"" + description + "\"的房子")
print()
return agency.getMatchInfos(agency.getSearchCondition(description))

def seeHouse(self, houseInfos):
"去看房,选择最使用的房子。(这里省略看房的过程)"
size = len(houseInfos)
return houseInfos[size - 1]

def signContract(self, houseInfo, agency, time):
"与中介签订协议"
print(self.getName(), "与中介", agency.getName(), "签订",
houseInfo.getAddress(), "的房子的租赁合同, 租期", time, "年。合同期内",
self.__name, "有权对其进行使用!")

测试代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
def testRenting():
myHome = HousingAgency("我爱我家")
zhangsan = HouseOwner("张三", "上地西里")
zhangsan.setHouseInfo(20, 2500, 1, "独立卫生间", 0)
zhangsan.publishHouseInfo(myHome)
lisi = HouseOwner("李四", "当代城市家园")
lisi.setHouseInfo(16, 1800, 1, "公用卫生间", 0)
lisi.publishHouseInfo(myHome)
wangwu = HouseOwner("王五", "金隅美和园")
wangwu.setHouseInfo(18, 2600, 1, "独立卫生间", 1)
wangwu.publishHouseInfo(myHome)
print()

myHome.signContracts(3)
print()

tony = Custom("Tony")
houseInfos = tony.findHouse("18平米左右,要有独卫,要有窗户,最好是朝南,有厨房更好!价位在2000左右",
myHome)
print()
print("正在看房,寻找最合适的住巢……")
print()
AppropriateHouse = tony.seeHouse(houseInfos)
tony.signContract(AppropriateHouse, myHome, 1)


testRenting()

输出结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
张三在 我爱我家 发布房源出租信息:
面积:20平米 价格:2500元 窗户:有 卫生间:独立卫生间 厨房:没有 地址:上地西里 房东:张三
李四在 我爱我家 发布房源出租信息:
面积:16平米 价格:1800元 窗户:有 卫生间:公用卫生间 厨房:没有 地址:当代城市家园 房东:李四
王五在 我爱我家 发布房源出租信息:
面积:18平米 价格:2600元 窗户:有 卫生间:独立卫生间 厨房:有 地址:金隅美和园 房东:王五

我爱我家 与房东 张三 签订 上地西里 的房子的的租赁合同,租期 3 年。 合同期内 我爱我家 有权对其进行使用和转租!
我爱我家 与房东 李四 签订 当代城市家园 的房子的的租赁合同,租期 3 年。 合同期内 我爱我家 有权对其进行使用和转租!
我爱我家 与房东 王五 签订 金隅美和园 的房子的的租赁合同,租期 3 年。 合同期内 我爱我家 有权对其进行使用和转租!

我是Tony, 我想要找一个"18平米左右,要有独卫,要有窗户,最好是朝南,有厨房更好!价位在2000左右"的房子

我爱我家 为您找以下最适合的房源:
面积:20平米 价格:2500元 窗户:有 卫生间:独立卫生间 厨房:没有 地址:上地西里
面积:16平米 价格:1800元 窗户:有 卫生间:公用卫生间 厨房:没有 地址:当代城市家园
面积:18平米 价格:2600元 窗户:有 卫生间:独立卫生间 厨房:有 地址:金隅美和园

正在看房,寻找最合适的住巢……

Tony 与中介 我爱我家 签订 金隅美和园 的房子的租赁合同, 租期 1 年。合同期内 Tony 有权对其进行使用!

从网状到星状

类图变化