• 文章
  • 从实际项目中学习设计模式
发布
2012 年 10 月 30 日(最后更新:2012 年 10 月 30 日)

从实际项目中学习设计模式:RigsOfRods 用例研究。

评分:3.5/5(164 票)
*****
大多数开发人员已经听说过设计模式,GOF(四人组)模式是最普及的,每个开发人员都有自己的学习方式,我们可以列举

  • 阅读书籍或杂志。
  • 从网站获取。
  • 从同事那里学习。
  • 参加培训。

无论选择哪种方法,我们都可以死记硬背模式并花费数小时来记住它们的 UML 图,但有时当我们在实际项目中需要使用它们时,就会变得更加困难。

最重要的是要准确地知道模式名称以及如何在文档中所述的那样实现它们,但更相关的是每个模式背后的动机,因为正是从动机中我们发明了模式。

为了更好地掌握模式动机,有趣的方式是从实际项目中研究它们。这就是本文的目标,我们将尝试发现一个大量使用它们的开源项目。

Rigs of Rods 分析


Rigs of Rods(“RoR”)是一款开源的多重模拟游戏,它使用软体物理来模拟车辆的运动和变形。该游戏采用一种称为 Beam 的特定软体物理引擎构建,该引擎模拟互连节点的网络(构成底盘和车轮),并能够模拟可变形对象。通过此引擎,车辆及其负载在施加应力时会弯曲和变形。撞到墙壁或地形可能会永久变形车辆。

在这里,我们将发现一些使用的 GOF 设计模式,为此我们使用了 CppDepend 来分析 RoR,而 CQLinq 将有助于查询代码库。

单例(Singleton)


单例是最流行也是最常用的。RoR 使用通用单例来避免为每个单例类重复相同的代码,并定义了两种变体:一种创建实例,另一种分配实例。


让我们使用以下 CQLinq 查询搜索所有 RoR 单例。
1
2
from t in Types where t.DeriveFrom("RoRSingletonNoCreation") || t.DeriveFrom("RoRSingleton")
select t



动机
以 InputEngine 单例为例,RoR 需要存储有关键盘、鼠标和操纵杆的信息,这些信息在初始化时由 InputEngine 类检测到,所有类都需要 InputEngine 类的相同数据,并且不需要创建多个实例,因此主要动机是“创建一个 InputEngine 类的实例”。

为了实现这一点,我们可以将其声明为全局变量或将其定义为单例,但是使用单例变得有争议,并且并非所有架构师和设计师都推荐它,这是一篇文章讨论了单例的争议。

工厂方法(Factory Method)


关于工厂没什么神秘的,它们的目标很简单:创建实例,一个包含 CreateInstance 方法的简单工厂就可以实现这个目标,但是 RoR 在其所有工厂中使用“工厂方法”模式而不是简单工厂。


动机
为了更好地理解这个模式,让我们描述一下 RoR 使用这个模式的场景

- RoR 使用图形引擎 OGRE,并且 OGRE 的一些类需要实例化 ParticleEmitter 类。
- RoR 定义并使用了另一个类 BoxEmitter,它继承自 ParticleEmitter,并希望 OGRE 使用这个新类作为 ParticleEmitter。
- OGRE 对 RoR 一无所知。

问题是,OGRE 将如何知道如何从 RoR 实例化这个新的 BowEmitter 类并使用它?

“工厂方法”模式的作用就在这里

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

RoR 通过 ParticleSystemManager::addEmitterFactory (ParticleEmitterFactory * factory) 将这个工厂提供给 OGRE。每次 OGRE 需要 ParticleEmitter 的实例时,都会调用 BoxEmitterFactory 来创建它。

最重要的动机是低耦合,实际上 OGRE 对 RoR 一无所知,它可以实例化其中的类。

使用简单工厂可以隔离实例化逻辑,但使用“工厂方法”更能促进低耦合。

模板方法(Template Method)


模板方法在方法中定义了算法的骨架,将某些步骤留给子类。模板方法允许子类重新定义算法的某些步骤,而无需更改算法的结构。

目的是确保算法的结构保持不变,同时子类提供部分实现。

让我们使用 CQLinq 检测所有使用模板方法模式的类。为此,我们可以搜索父类中使用了纯虚方法并有子类实现这些虚方法的类。

1
2
3
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 方法实现了模板方法模式,它调用了纯虚方法 processIRCEvent。


LobbyGui 是一个需要处理 IRC 事件的类,它继承自 IRCWrapper,处理接收到的 IRC 事件的逻辑集中在 IRCWrapper::process 中,但是对于收到的每个事件,LobbyGui 都必须处理它,因此 process 方法调用了被 LobbyGui 类重载的 processIRCEvent。

通过这种模式,我们可以轻松地更改算法的实现而无需更改骨架,它促进了低耦合,因为客户端可以仅引用抽象类而不是具体类。

策略(Strategy)


有些常见情况是类仅在行为上有所不同。对于这些情况,将算法隔离到单独的类中是一个好主意,以便能够动态地选择不同的算法。

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

1
2
3
4
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,这里是所有使用 IBehavior 类的 CameraManager 方法。


正如我们所观察到的,有一个名为 switchBehavior 的方法用于动态更改行为。

此模式促进低耦合,CameraManager 不知道具体的行为,并且还促进高内聚,因为每个特定行为都实现在一个隔离的类中。

状态


从架构角度来看,状态(State)模式与策略(Strategy)设计模式相似,因此,对于我们之前搜索策略模式的 CQLinq 查询,我们也找到了状态类。

但是目标不同,策略模式代表使用一个或多个 IStrategy 实现的算法,并且这些不同的行为之间没有相关性;但是对于状态模式,我们从一个状态传递到另一个状态以实现最终目标,因此不同的状态之间存在内聚性。

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


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


正如我们所观察到的,AppStateManager 包含许多管理状态生命周期的函数。

动机
与策略模式一样,此模式促进低耦合,AppState管理器不知道具体的类,并且还促进高内聚,因为每个处理都隔离在其对应的状态中。


外观(Facade)


外观是一个对象,它为更大的代码体(例如类库)提供简化的接口。要检测外观,最简单的方法是搜索使用的外部代码。

这里是 ROR 项目使用的所有命名空间。


让我们以 Caelum 为例,并从 RoR 中搜索使用它的类。

1
2
from m in Methods where m.IsUsing ("Caelum")
select new { m }



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

OIS 命名空间呢?

1
2
from m in Methods where m.IsUsing ("OIS")
select new { m }



主要是 InputManager 使用 OIS 命名空间。

动机
如果我们使用外部库,并且它与我们的代码高度耦合,即许多类直接使用该库,那么更改外部库将非常困难。但是,如果使用了外观,那么当我们想要更改外部库时,只会更改其实现。

因此,此模式促进了与外部库的低耦合。

领域驱动设计方法(Domain Driven Design approach)


领域驱动设计是一种软件设计方法,基于两个前提

- 复杂的领域设计应基于模型,并且
- 对于大多数软件项目,应将重点放在领域和领域逻辑上(而不是用于实现系统的特定技术)。

换句话说,DDD 的核心是模型,开发开始时要做的第一件事就是绘制模型。您创建的模型和设计应该互相塑造。模型应该代表业务知识。

RoR 使用这种方法并将所有数据隔离到结构中,并对此进行检查,让我们搜索所有 ROR 结构。

1
2
3
from t in Types where t.IsStructure && 
t.Methods.Where(a=>!a.IsGeneratedByCompiler).Count()==0 
select t


树状图视图显示了受此查询影响的所有代码,蓝色矩形代表 CQLinq 查询的结果。


结论


使用设计模式有很多优点,但如果不理解它们的动机,就很难实现它们,正如我们在本文中所发现的,低耦合和高内聚等动机几乎存在于所有设计模式中,因此,建议也探索更侧重于动机的模式,例如 GRASP,以完成我们对 GOF 的理解。