博客 阅读需 7 分钟

从 RigsOfRods 游戏项目学习设计模式

分享本文
Learn Design Patterns from RigsOfRods Game Project

大多数开发者都听说过设计模式;GOF(四人帮)模式是流传最广的,而每位开发者都有自己独特的学习方式。我们可以列举:

  • 读书。
  • 通过网站学习。
  • 向同事学习。
  • 参加培训课程。

无论选择哪种方式,我们都可以把模式背得滚瓜烂熟、花几个小时记住它们的 UML 图,但把它们应用到真实项目中可能更具挑战性。

最重要的不是记住模式的确切名称,也不是严格按照文档中的描述来实现它们;更重要的是理解每个模式背后的动机——模式正是从这些动机中诞生的。

更好地理解这些模式背后动机的一个好方法,是在真实项目中研究它们。这正是本文的目标:我们将探索一个大量使用这些模式的开源项目的源代码。

Rigs of Rods 分析

Rigs of Rods(“RoR”)是一个开源的多仿真游戏,使用软体物理来模拟车辆的运动和形变。游戏使用一个名为 Beam 的专用软体物理引擎构建,该引擎模拟一个相互连接的节点网络(构成底盘和车轮),并能模拟可变形物体。借助这个引擎,车辆及其负载在行驶中会像现实中一样弯曲和变形。

让我们来探究 RoR 使用的一些 GoF 设计模式。

单例模式

单例是最流行、使用最广泛的设计模式之一。RoR 使用泛型单例,以避免为每个单例类重复编写相同的代码。它定义了两个变体:一个自行创建新实例的单例,另一个则接受一个已创建好的实例。

让我们搜索 RoR 的所有单例。为此,我们可以使用CQLinq:

from t in Types where t.DeriveFrom(“RoRSingletonNoCreation“) || t.DeriveFrom(“RoRSingleton“)
select t
动机:

以 InputEngine 单例为例:RoR 需要存储键盘、鼠标和操纵杆的数据,这些设备在初始化时由 InputEngine 类检测。许多类需要同样的输入设备数据,而且没有必要创建多个实例,所以主要动机是“创建 InputEngine 类的一个实例”

然而,单例的使用已经变得有争议,并非所有架构师和设计师都推荐它;这里有一篇关于单例之争的文章

工厂方法模式

工厂背后没有什么神秘之处;它的目的很简单:创建实例。一个包含 CreateInstance 方法的简单工厂就能实现这个目标。然而,RoR 的所有工厂都使用了“工厂方法”模式,而不是简单工厂。

动机:

为了更好地理解这个模式,让我们看看 RoR 使用它的一个场景:

  • RoR 使用图形引擎OGRE,后者需要实例化 ParticleEmitter 一类的类。
  • RoR 定义了自己的 ParticleEmitter 类,名为 BoxEmitter,它继承自 ParticleEmitter,而 RoR 需要 OGRE 把这个新类当作 ParticleEmitter 来使用。
  • OGRE 对 RoR 一无所知。

问题是:OGRE 将如何知道如何实例化和使用这个来自 RoR 的新 BoxEmitter 类?这正是“工厂方法”模式的用武之地:

OGRE 有一个名为 ParticleEmitterFactory 的抽象类,提供 CreateEmitter 方法。为了完成工作,OGRE 需要一个具体的工厂。RoR 定义了一个新工厂 BoxEmitterFactory,继承自 ParticleEmitterFactory,并重写了 CreateEmitter 方法。

RoR 通过 ParticleSystemManager::addEmitterFactory(ParticleEmitterFactory *factory) 把这个工厂提供给 OGRE。每当 OGRE 需要一个 ParticleEmitter 实例时,就会调用 BoxEmitterFactory 来创建。

最重要的动机是低耦合;确实,OGRE 对 RoR 一无所知,却能实例化来自 RoR 的类。

另一个动机是通过把实例化委托给一个专门的工厂类来增强内聚性

使用简单工厂有助于隔离实例化逻辑、提高内聚性,但当同时需要低耦合时,“工厂方法”模式更为合适。

模板方法模式

模板方法在一个方法中定义算法的骨架,把某些步骤推迟到子类中实现。模板方法让子类能够在不改变算法结构的情况下,重新定义算法的某些步骤。

其目标是确保算法的结构保持不变,而由子类提供部分实现。

让我们使用CQLinq来检测所有使用模板方法模式的类。为此,我们可以搜索这样的抽象类(下图 UML 中的 Abstract 类):它们拥有一个或多个方法(图中的 templateMethod()),而这些方法调用了在子类中实现的一些方法(图中的 primitive1 和 primitive2)。

from t in Types where t.IsAbstract && t.Methods.Where(a=> a.NbLinesOfCode>0 && a.MethodsCalled.Where(b=>b.IsPureVirtual && b.ParentType==t).Count()>0).Count()>0 select t
动机:

以 IRCWrapper 类为例。它的 “process” 方法包含处理接收到的 IRC 事件的逻辑。下面是它调用的方法:

它调用纯虚方法 processIRCEvent,该方法必须由 IRCWrapper 的派生类实现。LobbyGui 就是这样一个类。它需要处理接收到的 IRC 事件,因此重写 processIRCEvent 方法来实现自己的特定行为。

使用这个模式,我们可以轻松改变算法的实现,而不必改变它的骨架。它减少了样板代码,让这些类更易于维护。

它还保持了低耦合,因为客户端可以只引用抽象类,而不是具体类。

策略模式

在许多情况下,各个类之间只有行为不同。在这种情况下,把算法隔离到独立的类中是个好主意,这样可以在运行时选择不同的算法。

让我们使用 CQLinq 检测所有使用策略模式的类。为此,我们可以搜索这样的抽象类:它们有多个派生类,而客户端引用的是抽象类而不是具体实现。

from t in Types where t.IsAbstract && t.DirectDerivedTypes.Count()>1 !t.IsThirdParty
let tt=t.DirectDerivedTypes
from db in tt where db.Methods.Where(a=>a.NbMethodsCallingMe!=0 !a.IsStatic).Count()==0
select new {db,t}
动机:

摄像机可以有多种行为——固定、自由、静态或等轴测——而且它的行为可以动态改变。未来还可以添加更多行为。

CameraManager 使用抽象的 IBehavior 类。下面是 CameraManager 所有使用 IBehavior 的方法。

可以看到,有一个名为 switchBehavior 的方法,可以动态改变行为。

这个模式保持了与外部库的低耦合——CameraManager 并不知道具体的行为——同时也保持了高内聚,因为每种特定的行为都在一个独立的类中实现。

状态模式

从架构角度看,状态模式与策略设计模式相似;因此,用前面搜索策略模式的那条 CQLinq 查询,我们也找到了状态类。

然而,它们的目标不同:策略模式表示一个使用一个或多个 IStrategy 实现的算法。这些不同行为之间没有关联;而在状态模式中,我们从一种状态转换到另一种状态以达成最终目标,因此不同状态之间存在联系。

下面是所有继承自抽象类 AppState 的状态类。

与策略模式一样,其他类只引用抽象类。下面是所有使用 AppState 的方法。

可以看到,AppStateManager 包含了若干管理状态生命周期的方法。

动机:

与策略模式一样,这个模式保持了低耦合——AppStateManager 并不知道具体的状态——同时也保持了高内聚,因为每个操作都被隔离在其对应的状态中。

外观模式

外观(facade)是一个为更大体量代码(如类库)提供简化接口的对象。识别项目中所用外观的一个简单方法,是搜索项目引用的外部代码。

下面是 RoR 项目使用的所有命名空间:

以 Caelum 命名空间为例,让我们搜索使用它的 RoR 类。

from m in Methods where m.IsUsing (“Caelum“)
select new { m }

只有 SkyManager 直接使用 Caelum 命名空间,因此它就是 Caelum 的外观。

动机

如果我们使用的外部库与代码紧密耦合——即许多类直接使用该库——那么替换它会非常困难。然而,如果使用了外观,当我们想替换外部库时,只需要修改外观的实现。

这个模式保持了与外部库的低耦合

结语

学完 GoF 模式之后,理解在代码中使用它们的动机是很有必要的。探究知名开源项目中模式的实现方式,可以帮助您更好地理解它们的价值。

分享本文