博客 阅读时间 5 分钟

OGRE 引擎:面向对象设计原则的案例研究

分享本文
OGRE Engine: A Case Study on OOP Principles

OGRE(面向对象图形渲染引擎)是一个面向场景、灵活的 3D 渲染引擎,用 C++ 编写,旨在让开发者更轻松、更直观地开发利用硬件加速 3D 图形的应用程序。该类库抽象了 Direct3D 和 OpenGL 等底层系统库的使用细节,并提供了基于世界对象和其他高级类的接口。

让我们用 CppDepend 来分析它,发掘它在设计上的优点。



OGRE3D 架构

依赖图展示了 OGRE3D 各项目之间的关系:



该架构是面向插件的,这对于在不修改内核项目“OgreMain”的情况下扩展 Ogre3D 非常有用。为了支持这种架构,OgreMain 项目提供了与插件通信所需的类。

让我们看看内核项目如何与插件通信——用以下查询搜索 RenderSystem_GL 插件所使用的 OgreMain 类:

SELECT TYPES WHERE IsDirectlyUsedBy "RenderSystem_GL"


该插件使用了一些工具类和模型实体,并重写了若干抽象类,以便将自己整合进 Ogre3D 生态系统。

让我们筛选下面的结果,只搜索 RenderSystem_GL 插件使用的抽象类:

SELECT TYPES WHERE IsDirectlyUsedBy "RenderSystem_GL" AND IsAbstract



插件可以重写场景、纹理、渲染行为等等。Ogre3D 提供了一套有趣的 API,可以轻松定制渲染,这让按我们的需求调整它变得非常容易。

继承与多态

使用面向对象的方法可能会导致为了充分利用多态而过度使用继承。

对于作为 Ogre3D 框架内核的 OgreMain 来说尤其如此——其中许多类就是被设计为由插件项目重写的。SELECT TYPES FROM PROJECTS "OgreMain" WHERE NbBaseClass >0





但多重继承会增加复杂性,我们必须谨慎使用。让我们搜索拥有多个基类的类。





只有少数类派生自多个类。

抽象度

对于面向插件的架构,宿主必须拥有大量抽象类,才能更加灵活、更具可扩展性。

让我们在 OgreMain 中搜索抽象类:SELECT TYPES FROM PROJECTS "OgreMain" WHERE IsAbstract





命名空间分层

CppDepend 提供了 DSM 图,我们可以对该矩阵进行三角化处理,聚焦标红的边框——它们会突出显示依赖环。



Ogre、Ogre::EmitterCommands 和 Ogre::OverlayElementCommands 之间存在一个依赖环。存在这样的依赖不一定是问题,但避免这类依赖有助于保持松耦合;这篇有趣的文章解释了分层的好处。

让我们搜索 Ogre 与另外两个命名空间之间依赖关系的来源。SELECT TYPES WHERE IsDirectlyUsedBy "Ogre"





Ogre 命名空间使用了另外两个命名空间中的所有 Cmd 类,而这些类都被 Ogre::ParticleEmitter 用作静态字段;ParticleEmitter 将它们添加到 CmdParam 字典中。

或许可以采用不同的实现方式来避免这个依赖环,尽管它并不是什么大问题。

类型内聚性

单一职责原则指出,一个类不应有超过一个变更理由。这样的类被称为内聚的。较高的 LCOM 值通常表明类的内聚性较差。LCOM 度量有好几种:LCOM 的取值范围是 [0-1];LCOMHS(HS 代表 Henderson-Sellers)的取值范围是 [0-2]。请注意,LCOMHS 度量通常被认为在检测非内聚类型方面更有效。LCOMHS 值高于 1 就应视为警讯。SELECT TYPES WHERE LCOMHS > 0.95 AND NbFields > 10 AND NbMethods >10 AND !IsGlobal





只有少数类被认为内聚性较低。

使用到的设计模式

单例模式

要确保一个类只有一个实例,最好的方式是使用单例模式。

让我们搜索作为单例的类:

SELECT TYPES WHERE DeriveFrom "Ogre.Singleton"





可以看到,几乎所有管理器类都是单例——一般来说,管理器是成为单例的绝佳候选。我们还可以搜索那些没有派生自 Singleton 的管理器类 SELECT TYPES WHERE !DeriveFrom "Ogre.Singleton" AND NameLike "Manager$"





只有少数管理器没有派生自 Singleton,这是正常的,因为那些类可以被多次实例化。

工厂模式

工厂模式对于抽象对象的创建过程非常有用;它能促进低耦合和高内聚,如这篇文章所述。



然而,拥有工厂并不保证实例一定通过它创建,因为类仍然可以被直接实例化。因此,我们需要定义一条规则来确保实例化经由工厂进行。

例如,对于 Entity 类,我们可以定义以下规则,找出每个实例化它的类:SELECT METHODS WHERE DepthOfCreateA "Ogre.Entity" == 1





可以看到,只有 EntityFactory 实例化了 Entity 类。

管理器模式

管理器类提供对各个子系统的访问,对于项目的模块化非常有用。Ogre3D 包含许多管理器,每一个都代表一个不同的子系统。

SELECT TYPES WHERE NameLike "Manager$"





外观模式

外观(facade)定义了一个让子系统更易使用的高层接口;我们可以使用传出耦合(Efferent Coupling)度量来检测项目中的外观。某个类型的传出耦合是它直接依赖的类型数量。TypeCe > 50 的类型依赖了过多的其他类型。它们很复杂,承担着不止一项职责,是重构的良好候选。

不过,对于外观类来说,较高的 TypeCe 是正常的。

让我们搜索 TypeCe 较高的类:SELECT TYPES ORDER BY TypeCe DESC





并非所有这些类都是外观,每个类的 TypeCe 偏高也许都有合理的解释。其中一些或许可以重构,但我们对 Ogre3D 的了解还不足以妄下结论。

使用最广泛的外观是 Root 类;让我们看看它使用了哪些子系统。



可以看到,Root 类几乎使用了所有管理器类。

我们还可以用以下 CQL 查询,搜索无法通过 Root 访问的管理器:

SELECT TYPES WHERE !IsDirectlyUsedBy "Ogre.Root" AND NameLike "Manager$" AND !IsAbstract





观察者模式

观察者(observer)是这样一种模式:一个称为主体的对象维护一份依赖者(称为观察者)列表,并在状态发生任何变化时自动通知它们——通常是调用观察者的某个方法。它主要用于实现事件处理系统。

Ogre3D 使用 Listener 类来实现观察者模式。

让我们在 OgreMain 项目中搜索 Listener 类。SELECT TYPES FROM PROJECTS "OgreMain" WHERE NameLike "Listener$"





结语

Ogre3D 是一个非常整洁、设计良好且文档完备的框架。您可以轻松理解它所使用的各种设计模式的意图,而它的模块化也能帮助您更快地掌握它的功能。

分享本文