设计模式是在软件设计中针对特定情境下常见问题的通用可复用解决方案。模式是形式化的最佳实践,程序员可以用它们来解决设计应用或系统时的常见问题。
"四人组"(Gang of Four)模式或许是最流行的。然而,有一些基本设计原则在开发者中并不广为人知:通用职责分配软件原则(General Responsibility Assignment Software Principles),简称 GRASP。
以下是维基百科的定义:
通用职责分配软件模式(或原则),缩写为GRASP,由面向对象设计中为类和对象分配职责的指南组成。GRASP 中使用的不同模式和原则包括:控制器(controller)、创建者(creator)、间接性(indirection)、信息专家(information expert)、高内聚、低耦合、多态、受保护的变化(protected variations)和纯虚构(pure fabrication)。所有这些模式都回答了一些软件问题,而这些问题几乎是每个软件开发项目都会遇到的。这些技术的发明不是为了创造新的工作方式,而是为了更好地记录和标准化面向对象设计中那些久经考验的编程原则。
Irrlicht是一个使用了许多 GRASP 原则的 3D 引擎库;让我们来发现使用这类模式的好处。
创建者(Creator)
创建对象是面向对象系统中最常见的活动之一。确定哪个类负责创建对象是特定类对象之间关系的基本方面。让我们以 GUI 皮肤类为例,看看它在 Irrlicht 库中的哪里被创建。为此,我们可以执行这个 CQLinq 查询:
SELECT METHODS WHERE DepthOfCreateA “irr.gui.CGUISkin” == 1

CGUIEnvironment 是唯一创建 CGUISkin 实例的类,而且几乎所有 GUI 元素都由 CGUIEnvironment 类创建,除了 CGUIButton。
SELECT METHODS WHERE DepthOfCreateA “irr.gui.CGUIButton” == 1

如我们所见,CGUIButton 在三个不同的地方被创建;也许最好重构代码,将创建职责委托给 CGUIEnvironment 类,就像所有其他 GUI 类一样。
控制器(Controller)
控制器模式将处理系统事件的职责分配给一个代表整个系统或用例场景的非 UI 类。控制器对象是负责接收或处理系统事件的非用户界面对象。
应该使用用例控制器来处理所有用例的系统事件,并且它可以用于多个用例(例如,对于创建用户和删除用户用例,可以只用一个UserController,而不是两个独立的用例控制器)。
让我们来找出 GUI 元素的控制器,它至少要管理事件处理;这个处理由 CGUIEnvironment::OnEvent 执行。
让我们看看 OnEvent 使用了哪些方法:
SELECT METHODS WHERE IsUsedBy “irr.gui.CGUIEnvironment.OnEvent(constSEvent&)”

触发的事件由实现 IEventReceiver 抽象类的类处理。让我们探究哪些类实现了它。

每个 GUI 元素处理与其相关的事件。
CGUIEnvironment 还有哪些其他职责?
如我们之前所见,这个类创建具体类,还管理事件处理;要发现它是否有其他职责,我们可以搜索该类使用的方法:
SELECT METHODS WHERE IsDirectlyUsedBy “irr.gui.CGUIEnvironment”

这个类还使用了 irr::io 命名空间中的一些类来持久化和加载 XML 文件。也许这个类承担了太多职责,这可能影响其内聚性,但这仍然是可以接受的,因为这个类拥有持久化数据所需的所有数据——它遵循了 GRASP 的"信息专家"原则。
低耦合
低耦合是理想的,因为应用程序某一区域的变更将只需要在整个应用程序中做更少的改动。从长远来看,这可以节省大量与修改应用程序和添加新功能相关的时间、精力和成本。
使用抽象类可以帮助实现低耦合,我们可以用以下度量来评估给定模块的抽象程度:
A = Na / Nc其中:* A = 模块的抽象度。零表示完全具体的模块,一表示完全抽象的模块。* Na = 模块中抽象类的数量。* Nc = 模块中具体类的数量。
Irrlicht 的抽象度为 0.1245972,包含 125 个抽象类。在 irr::gui 命名空间中,有 28 个抽象类——每个 GUI 元素都有一个对应的接口。
SELECT TYPES FROM NAMESPACES “irr.gui” WHERE IsAbstract

CppDepend 提供 DSM(依赖结构矩阵),我们可以对这个矩阵进行三角化,以聚焦于以红色边框标出的高依赖类,并检测模块。

如我们所见,所有抽象类被分组在一起,它们可以被隔离到另一个命名空间,或者也许是另一个项目中。为了看到使用抽象类实现低耦合的好处,让我们搜索使用具体类 CGUISkin 的类。
SELECT METHODS WHERE IsDirectlyUsing “irr.gui.CGUISkin”

只有一个类直接知道这个类:它的创建者。其他具体类都通过抽象类被使用,这促进了松耦合。
命名空间之间的耦合情况如何?

某些命名空间之间存在依赖环;存在这样的依赖不一定是问题。然而,避免它可以促进松耦合。
我们还可以了解命名空间之间是如何交互的。为此,让我们看看"irr"命名空间使用了哪些类和方法。
SELECT METHODS WHERE IsDirectlyUsedBy “irr”

如我们所见,几乎所有与其他命名空间的交互都通过抽象类进行,除了 irr::video::CVideoModeList 和 irr::scene::CMeshBuffer。
让我们来找出对 irr::video::CVideoModeList 依赖的来源。为此,我们可以执行以下 CQLinq 查询:
SELECT METHODS OUT OF TYPES “irr.video.CVideoModeList” WHERE IsUsing “irr.video.CVideoModeList”

irr::CIrrDeviceWin32 类使用了它,因为它将一个字段声明为 video::CVideoModeList 而不是 video::IVideoModeList。也许可以进行一次重构来改善命名空间之间的交互。
高内聚
单一职责原则指出,一个类不应该有多于一个的变更原因。这样的类被称为内聚的。高 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 ORDER BY LCOMHS DESC

只有少数类被认为内聚性不佳。
结论
Irrlicht 使用命名空间来模块化代码库,使用抽象类来改善低耦合,这使它非常易于理解和维护。如果您想提高自己的设计质量,这是一个值得效仿的好例子。
