思考并回答以下问题:
- 中介者模式的官方定义是什么?
- 中介者模式属于什么类型的模式?
本章涵盖:
- 游戏系统之间的沟通
- 中介者模式
- 中介者模式的定义
- 中介者模式的说明
- 中介者模式的实现范例
- 中介者模式作为系统之间的沟通接口
- 使用中介者模式的系统架构
- 实现说明
- 使用中介者模式的优点
- 实现中介者模式时的注意事项
- 中介者模式面对变化时
- 结论
游戏系统之间的沟通
在游戏主要类--外观模式曾提到过,《P级阵地》将整个游戏需要执行的系统切分成好几个,包含的游戏系统如下:
- 游戏事件系统(GameEventSystem);
- 兵营系统(CampSystem);
- 关卡系统(StageSystem);
- 角色管理系统(CharacterSystem);
- 行动力系统(APSystem);
- 成就系统(AchievementSystem)。
另外,还有之前没提到过的,用来与玩家互动的界面:
- 兵营界面(CampInfoUI);
- 战士信息界面(SoldierlnfoUI);
- 游戏状态界面(GameStateInfoUI);
- 游戏暂停界面(GamePauseUI)。
回顾单一职责原则(SRP)强调的是,将系统功能细分、封装,让每一个类都能各司其职,负责系统中的某一项功能。因此,一个分析设计良好的软件或游戏,都是由一群子功能或子系统一起组合起来运行的。
整个游戏系统在面对客户端时,可以使用第4章提到的外观模式(Facade)整合出一个高级接口供客户端使用,减少它们接触游戏系统的运行,并加强安全性及减少耦合度。但对于内部子系统之间的沟通,又该如何处理呢?
在《P级阵地》规划的游戏系统中,有些系统在运行时,需要其他系统的协助或将信息传递给其他系统。例如,玩家想要产生战士:
①兵营界面(CampInfoUI)接收到玩家的指令后,
②向兵营系统(CampSystem)发出要训练一名战士的需求。
③兵营系统(CampSystem)接收到通知后,向行动力系统(APSystem)询问是否有足够的行动力可以生产。
④行动力系统(APSystem)回复有足够的行动力后,
⑤兵营系统(CampSystem)便执行产生战士的功能,
⑥然后通知行动力系统(APSystem)扣除行动力,
⑦接着通知游戏状态界面(GameStateInfoUI)显示当前的行动力。
⑧最后则是将产生的战士交给角色管理系统(CharacterSystem)来管理。
上述的8个流程中,一共有3个游戏系统及2个玩家界面参与其中运行,如图1所示。
图1 游戏运行流程中游戏系统参与运行的示例图
因为项目一开始时,各系统是慢慢构建起来的,所以可能会实现下列程序代码:
Listing 1 内部系统交错使用的情况
1 | // 兵营界面 |
从上面的程序代码可以看出,所有系统在实现上都必须引用其他系统的对象,而这些被引用的对象都必须在功能执行前设置好,或者在调用方法时通过参数传入。但这些方式都会增加系统之间的依赖程度,也与最少知识原则(LKP)有所抵触。
上面的流程只呈现了《P级阵地》众多功能中的一个。如果将各个功能执行时所需要连接的系统,都绘制成关联图的话,最后可能如图2所示。如果我们运用计算多边形各个顶点连线条数(或者连接数)的公式,应该能获知系统间的复杂度是多少。
图2 各个系统设计时依赖性或关联性过大的极端情况
系统切分越细,则意味着系统之间的沟通越复杂,如果系统内部持续存在这样的连接,就会产生以下缺点:
- 单一系统引入太多其他系统的功能,不利于单一系统的转换和维护;
- 单一系统被过多的系统所依赖,不利于接口的更改,容易牵一发而动全身;
- 因为需提供给其他系统操作,系统的接口可能会过于庞大,不容易维护。
要解决上述问题,可以使用中介者模式的设计方式。
中介者模式(Mediator)简单解释的话,比较类似于中央管理的概念。建立一个信息集中的中心,任何子系统要与它的子系统沟通时,都必须先将请求交给中央单位,再由中央单位分派给对应的子系统。这种交给中央单位统一分配的方式,在物流业中已证明是最有效率的方式,如图3所示。
图3 物流业的货物流动示意图
同样地,《P级阵地》的子系统也希望在运用中介者模式后,能够由统一的接口来进行接收和转发信息,如图4所示。
图4 《P级阵地》运用中介者模式后系统间关联性的示意图
中介者模式
刚开始学习中介者模式时,会觉得为什么要如此麻烦,让两个功能直接调用就好了。但随着经验的累积,接触过许多项目,并且想要跨项目转换某个功能时就会知道,减少类之间的耦合度是一项很重要的设计原则。中介者模式在内部系统的整合上,扮演着重要的角色。
中介者模式的定义
中介者模式在GoF中的说明是:1
定义一个接口用来封装一群对象的互动行为。中介者通过移除对象之间的引用,来减少它们之间的耦合度,并且能改变它们之间的互动独立性。
以运输业的运营方式来说明中介者模式,可以解释为:1
设置一个物品集货中心,让所有收货点的物品都必须先集中到集货中心后,再分配出去,各集货点之间不必知道其他集货点的位置,省去各自在货物运送上的浪费。
以一个拥有上百个集货点的货运行来说,各集货点不必自行运送到其他点,统一送到中央集货中心(或物流中心)后再分送出去,才是比较有效率的方式。
中介者模式的说明
中介者模式的结构如图5所示。
图5 中介者模式的结构示意图
参与者的说明如下:
- Colleague(同事接口)
- 拥有一个Mediator属性成员,可以通过它来调用中介者的功能。
- ConcreteColleagueX(同事接口实现类)
- 实现Colleague接口的类,对于单一实现类而言,只会依赖一个Mediator接口。
- Mediator(中介者接口)、ConcreteMediator(中介者接口实现类)
- 由Mediator定义让Colleague类操作的接口。
- ConcreteMediator实现类中包含所有ConcreteColleague的对象引用。
- ConcreteColleague类之间的互动会在ConcreteMediator中发生。
中介者模式的实现范例
在GoF范例程序中,Colleague(同事接口)如下:
Listing 2 Mediator所控管的Colleague(Mediator.cs)
1 | public abstract class Colleague |
Colleague为抽象类,拥有一个类型为Mediator的属性成员m_Mediator,用来指向中介者,而这个中介者会在构造函数中被指定。
ConcreateColleague1、ConcreateColleague2继承了Colleague类,并重新定义父类中的抽象方法:
Listing 3 实现各Colleague类(Mediator.cs)
1 | // 实现Colleague的类1 |
每一个继承自Colleague的ConcreteColleagueX类,需要对外界沟通时,都会通过m_Mediator来传递信息。而来自Mediator的请求也会通过父类的抽象方法Request()来进行通知。
以下是Mediator的接口:
Listing 4 用来管理Colleague对象的接口(Mediator.cs)
1 | public abstract class Mediator |
Mediator定义了一个抽象方法SendMessage(),主要用于从外界传递信息给Colleague。
最后实现ConcreteMediator类,该类拥有所有“要在内部进行沟通的Colleague子类的引用”:
Listing 5 实现Mediator接口,并集合管理Colleague对象(Mediator.cs)
1 | public class ConcreteMediator : Mediator |
因为测试程序只实现两个子类,所以在SendMessage中只是进行简单地判断,然后就转发给另一个Colleague。但在实际应用时,Colleague类会有许多个,必须使用别的转发方式才能提升效率,在后面的章节中会有相关的说明。以下是测试程序:
Listing 6 中介者模式的测试(MediatorTest.cs)
1 | void UnitTest () |
先产生中介者ConcreteMediator的对象之后,接着产生两个Colleague对象,并将其设置给中介者。分别调用两个Colleague对象的Action方法,查看信息是否通过Mediator传递给另一个Colleague类:
执行结果 中介者模式的测试执行结果
1 | ConcreateColleague2.Request:Colleague1发出通知 |
Console窗口上会显示两个Colleague类发出的信息,表示都已正确地接收了另一个类传送过来的信息。
中介者模式作为系统之间的沟通接口
在游戏主要类--外观模式的介绍中,说明了如何将PBaseDefenseGame类运用外观模式让游戏系统整合在单一接口之下,“对外”作为对客户端的操作接口时使用。而在本章中,则是将PBaseDefenseGame类运用中介者模式让其“对内”也成为游戏系统之间的沟通接口。
使用中介者模式的系统架构
经过重新分析设计之后,PBaseDefenseGame类的中介者模式将串接《P级阵地》中的两个主要的类群组:“游戏系统”与“玩家界面”,如图6所示。
图6 PBaseDefenseGame类的中介者模式,串接《P级阵地》中的两个主要的类群组:“游戏系统”与“玩家界面”。
参与者的说明如下:
- PBaseDefenseGame:担任中介者角色,定义相关的操作接口给所有游戏系统与玩家界面来使用,并包含这些游戏系统和玩家界面的对象,同时负责相关的初始化流程。
- IGameSystem:游戏系统的共同父类,包含一个指向PBaseDefenseGame对象的类成员,在其下的子类都能通过这个成员向PBaseDefenseGame发出需求。
- GameEventSystem、CampSystem、…:负责游戏内的系统实现,这些系统之间不会互相引用及操作,必须通过PBaseDefenseGame来完成。
- IUserInterface:玩家界面的共同父类,包含一个指向PBaseDefenseGame对象的类成员,在其下的子类都能通过这个成员向PBaseDefenseGame发出需求。
- SoldierInfoUI,CampInfoUI、…:负责各玩家界面的实现,这些玩家界面与游戏系统之间不会互相引用及操作,必须通过PBaseDefenseGame来完成。
实现说明
以下是PBaseDefenseGame类在实现中介者模式后的程序代码: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
42public class PBaseDefenseGame
{
// 游戏系统
private GameEventSystem m_GameEventSystem = null; // 游戏事件系统
private CampSystem m_CampSystem = null; // 兵营系统
private StageSystem m_StageSystem = null;// 关卡系统
private CharacterSystem m_CharacterSystem = null; // 角色管理系统
private APSystem m_ApSystem = null; // 行动力系统
private AchievementSystem m_AchievementSystem = null; // 成就系统
// 界面
private CampInfoUI m_CampInfoUI = null; // 兵营界面
private SoldierInfoUI m_SoldierInfoUI = null; // 战士信息界面
private GameStateInfoUI m_GameStateInfoUI = null; // 游戏状态界面
private GamePauseUI m_GamePauseUI = null; // 游戏暂停界面
// 初始化P-BaseDefense游戏的相关设置
public void Initinal()
{
// 场景状态控制
m_bGameOver = false;
// 游戏系统
m_GameEventSystem = new GameEventSystem(this); // 游戏事件系统
m_CampSystem = new CampSystem(this); // 兵营系统
m_StageSystem = new StageSystem(this); // 关卡系统
m_CharacterSystem = new CharacterSystem(this); // 角色管理系统
m_ApSystem = new APSystem(this); // 行动力系统
m_AchievementSystem = new AchievementSystem(this); // 成就系统
// 界面
m_CampInfoUI = new CampInfoUI(this);// 兵营信息
m_SoldierInfoUI = new SoldierInfoUI(this); // Soldier信息
m_GameStateInfoUI = new GameStateInfoUI(this); // 游戏数据
m_GamePauseUI = new GamePauseUI (this); // 游戏暂停
// 注入到其它系统
EnemyAI.SetStageSystem( m_StageSystem );
...
}
...
}
类内包含所有游戏系统及玩家界面等对象,并负责它们的产生和初始化,另外也提供了游戏系统之间相互沟通时的方法: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...
// 升级Soldier
public void UpgradeSoldier( ISoldier theSoldier)
{
if(m_CharacterSystem != null)
m_CharacterSystem.UpgradeSoldier( theSoldier );
}
// 增加Soldier
public void AddSoldier( ISoldier theSoldier)
{
if( m_CharacterSystem !=null)
m_CharacterSystem.AddSoldier( theSoldier );
}
// 删除Soldier
public void RemoveSoldier( ISoldier theSoldier)
{
if( m_CharacterSystem !=null)
m_CharacterSystem.RemoveSoldier( theSoldier );
}
// 增加Enemy
public void AddEnemy( IEnemy theEnemy)
{
if( m_CharacterSystem !=null)
m_CharacterSystem.AddEnemy( theEnemy );
}
// 删除Enemy
public void RemoveEnemy( IEnemy theEnemy)
{
if( m_CharacterSystem !=null)
m_CharacterSystem.RemoveEnemy( theEnemy );
}
...
上面几个是游戏玩家单位Soldier和敌方单位Enemy相关操作的方法。从实现中可以看到,这几个方法主要是转发给角色管理系统(CharacterSystem)做后续的处理,而这些方法都可以由其他游戏系统或玩家界面调用。
在操作游戏系统或玩家界面时,可以同时转发给不止一个的系统或界面。为了满足游戏设计的需求,可以同时通知不同的子系统和玩家界面:1
2
3
4
5
6
7
8
9
10
11
12
13// 显示兵营信息
public void ShowCampInfo( ICamp Camp )
{
m_CampInfoUI.ShowInfo( Camp );
m_SoldierInfoUI.Hide();
}
// 显示Soldier信息
public void ShowSoldierInfo( ISoldier Soldier )
{
m_SoldierInfoUI.ShowInfo( Soldier );
m_CampInfoUI.Hide();
}
为了能够更灵活地处理游戏系统之间的沟通,《P级阵地》也实现了观察者模式(Observer),游戏事件系统(GameEventSystem)即观察者模式的类。通过它能减少在PBaseDefenseGame中增加接口方法,并且让信息的通知更有效率。而它的相关操作也是通过PBaseDefenseGame提供的方法来完成的:1
2
3
4
5
6
7
8
9
10
11
12// PBaseDefenseGame.cs
// 注册游戏事件
public void RegisterGameEvent( ENUM_GameEvent emGameEvent, IGameEventObserver Observer)
{
m_GameEventSystem.RegisterObserver( emGameEvent , Observer );
}
// 通知游戏事件
public void NotifyGameEvent( ENUM_GameEvent emGameEvent, System.Object Param )
{
m_GameEventSystem.NotifySubject( emGameEvent, Param);
}
IGameSystem类和IUserInterface类,分别作为“游戏系统类”和“玩家界面类”的共同接口:
Listing 7 游戏系统共享接口(IGameSystem.cs)
1 | public abstract class IGameSystem |
Listing 8 玩家界面的操作接口定义(IUserInterface.cs)
1 | // 游戏使用者界面 |
在这两个类中,都包含一个指向PBaseDefenseGame对象的类成员m_PBDGame,在各个子类对象产生的同时就必须完成设置。这两个类也都定义了提供客户端使用的方法,部分方法必须由子类继承后重新定义。
下面是继承自IGameSystem类的关卡控制系统(StageSystem):
Listing 9 关卡控制系统的实现(StageSystem.cs)
1 | public class StageSystem : IGameSystem |
在关卡系统初始化的过程中(在Initialize方法中),通过在父类中指向PBaseDefenseGame的属性成员m_PBDGame,来调用游戏事件注册功能:1
2
3
4
5
6public override void Initialize()
{
...
// 注册游戏事件
m_PBDGame.RegisterGameEvent( ENUM_GameEvent.EnemyKilled, new EnemyKilledObserverStageScore(this));
}
关卡系统在《P级阵地》中是负责战斗场景关卡的更新功能。所以,在每次关卡系统“定时更新”时,会判断是否需要产生新的关卡。除了通过m_PBDGame获取当前敌方单位的数量外,当系统决定要转换到下一个关卡时(在Notify NewStage方法中),也会利用m_PBDGame来通知当前关卡已经更新,并通知其他相关的系统。
每个游戏系统都有一个定期更新的方法,Update可以重新定义。这个机制是在《P级阵地》中特别设计的,主要是提供“单一的游戏系统”更新使用。其中一部分的说明,我们将在游戏的主循环-Game-Loop中进行介绍。
类似地,在玩家界面中,游戏状态信息(GameStateInfoUI)负责游戏相关信息的呈现: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// GameStateInfoUI.cs
// 游戏状态信息
public class GameStateInfoUI : IUserInterface
{
public override void Update ()
{
base.Update ();
...
// 双方数量
m_SoldierCountText.text = string.Format("我方单位数:{0}", m_UnitCountVisitor.GetUnitCount( ENUM_Soldier.Null ));
m_EnemyCountText.text = string.Format("敌方单位数:{0}", m_UnitCountVisitor.GetUnitCount( ENUM_Enemy.Null ));
}
...
// Continue
private void OnContinueBtnClick()
{
Time.timeScale = 1;
// 换回开始State
m_PBDGame.ChangeToMainMenu();
}
// Pause
private void OnPauseBtnClick()
{
// 显示暂停
m_PBDGame.GamePause();
}
...
}
运行上也是通过父类的属性成员m_PBDGame向PBaseDefenseGame类获取游戏相关信息或发出转换接口的请求。除此之外,并没有直接与其他游戏系统或玩家界面类相关的互动。
使用中介者模式的优点
在本章中,将PBaseDefenseGame类运用中介者模式,具备以下优点:
不会引入太多其他的系统
从上面《P级阵地》的实现来看,每一个游戏系统和玩家界面除了会引用与本身功能相关的类外,无论是对外的信息获取还是信息的传递,都只通过PBaseDefenseGame类对象来完成。这使得每一个游戏系统、玩家界面对外的依赖度缩小到只有一个类(PBaseDefenseGame)。
系统被依赖的程度也降低
每一个游戏系统或玩家界面,也只在PBaseDefenseGame类的方法中被调用。所以,当游戏系统或玩家界面有所更动时,受影响的也仅仅局限于PBaseDefenseGame类,因此可以减少系统维护的难度。
实现中介者模式时的注意事项
由于PBaseDefenseGame类担任中介者的角色,再加上各个游戏系统和玩家界面都必须通过它来进行信息交换及沟通,所以要注意的是,PBaseDefenseGame类会因为担任过多中介者的角色而容易出现“操作接口爆炸”的情况。因此,在实现上,我们可以搭配其他设计模式来避免发生这种情况。在前面的说明中,我们提及的游戏事件系统(GameEventSystem),其作用就是用来提供更好的信息传递方式,以减轻PBaseDefenseGame类的负担。
在GoF的实现结构图上,存在一个中介者接口类,但PBaseDefenseGame类却没有继承任何一个中介者接口,这是为什么呢?请读者回顾第5章中所提到的:为了呈现单例模式(Singleton)在《P级阵地》中的使用情形,将PBaseDefenseGame类运用单例模式,而单例模式的特性之一是“返回实现类”,因此PBaseDefenseGame没有继承任何接口类。不过,如果能删除单例模式的应用,将PBaseDefenseGame转化成一个接口类,那么对于所有的游戏系统和玩家界面而言,它们所依赖的将是“接口”而不是“实现”,这样会更符合开一闭原则(OCP),从而提高游戏系统和玩家界面的可移植性。
中介者模式面对变化时
任何软件系统都会面临需求的变化,采用中介者模式设计的软件同样会面对这些变化。在本节中,我们将探讨中介者模式如何面对变化,以及如何面对更常见的“新增子类”这种变化。
如何应对变化
当游戏系统或玩家界面需要新增功能,且该功能需要由外界提供信息才能完成时,可以先在PBaseDefenseGame类中增加获取信息的方法,之后再通过PBaseDefenseGame类来获取信息完成新的功能。这样一来,项目的修改可以保持在两个类或最多3个类的更改,而不会影响任何类的“依赖性”。
如何面对新增
当需要新增加游戏系统或玩家界面时,只要是继承自IGameSystem或IUserInterface的游戏系统和玩家界面,都可以直接加入PBaseDefenseGame的类成员中,并通过现有的接口进行实现或增加功能。这时候项目更改的幅度,可能只是新增一个程序文件和修改一个PBaseDefenseGame类而己,不太容易影响到其他系统或接口。
结论
中介者模式的优点是能让系统之间的耦合度降低,提升系统的可维护性。但身为模式中的中介者角色类,也会存在着接口过大的风险,此时必须再配合其他模式来进行优化。
与其他模式的合作
PBaseDefenseGame类在《P级阵地》中,除了是中介者模式中的中介者之外,也是外观模式中对外系统整合接口的主要类,并且还运用单例模式来产生唯一的类对象。
此外,为了降低PBaseDefenseGame类有接口过大的问题,其子系统“游戏事件系统”(GameEventSystem)专门运用观察者模式来解决游戏系统之间,对于信息的产生和通知的需求,减少这些信息和通知的方法充满在PBaseDefenseGame类之中。
在进行分析设计时,集合多种设计模式是良好设计常见的方式,如何将所学设计模式融合并适当地运用,才是设计模式之道。
其他应用方式
- 网络引擎:连线管理系统与网络数据封包管理系统之间,如果可以通过中介者模式进行沟通,那么就能轻松地针对连线管理系统抽换所使用的通信方式(TCP或UDP)。
- 数据库引擎:内部可以分成数个子系统,有专门负责数据库连接的功能与产生数据库操作语句的功能,两个子功能之间的沟通可以通过中介者模式来进行,让两者之间不相互依赖,方便抽换另一个子系统。