如今,每种编程语言都有许多成熟的库和框架,语言本身也加入了许多高级特性。但那些老项目呢?在语言特性远不如今天先进、成熟库寥寥无几的年代,它们又是怎样的?
让我们一起来探索一些实现精良的老项目,看看它们当年是如何实现的。
《波斯王子》(Prince of Persia)
《波斯王子》是一款奇幻平台游戏,最初由 Jordan Mechner 开发,于 1989 年在 Apple II 上发布,它代表了当时电子游戏动画质量的一次巨大飞跃。2012 年 4 月 17 日,Jordan Mechner公开了源代码。
许多玩家都记得这款令人惊叹的游戏,也许你们中的一些人曾连续玩上好几个月。

那个年代的开发者还记得,一台典型的个人电脑可能只有 8 MHz 的处理器、1 MB 内存、20 MB 硬盘和一个软盘驱动器。在这样的条件下开发《波斯王子》这样的游戏是一个巨大的挑战。
此外,那时候还没有谷歌可以帮助开发者快速解决技术问题;有些技术难题可能要花上开发者好几天时间才能修复。更了不起的是,这款游戏是用 6502 汇编语言开发的。
尽管存在这些限制,源代码的实现却非常出色:
- 它使用目录和文件进行了模块化:
模块化是一种软件设计技术,它提高软件由独立部分组合而成的程度;模块化代码更易于管理和维护。《波斯王子》使用目录和文件进行模块化——这种模块化由操作系统提供,可以应用于任何语言。
代码被拆分为许多文件,下面是其中一部分文件的列表:

- 命名易于理解
在浏览源代码时,你不会找到诸如 a、b 或 x 这样的变量名,这与许多最近开发的项目不同。名字选得很好,也不需要注释来解释为什么要用它们。

- 代码被拆分为许多小型子例程
6502 汇编语言非常底层,为了让代码更易于理解和维护,开发者运用了"分而治之"原则。事实上,代码被拆分为许多小型子例程,这使得它们易于阅读和维护。下面是源代码中一个小型子例程的例子:

即使在 1989 年,诸多限制让开发者的工作困难重重,代码依然实现得非常出色。那么为什么到了 2014 年,拥有强大的计算机、强大的语言、成千上万的库和谷歌,仍有一些项目实现得很糟糕呢?
语言和框架只是构建应用程序的工具,真正的主角是开发者。你可以使用最好的语言和最好的框架,却依然写出糟糕的代码。
许多让代码整洁的实践并不依赖于具体语言。一名优秀的开发者必须具备良好的素养,无论使用什么语言,都要让代码整洁且易于理解。
《毁灭战士 3》(Doom 3)
《毁灭战士 3》是由id Software开发、Activision发行的一款电子游戏。这款游戏为 id Software 带来了商业上的成功,销量超过 350 万份。

2011 年 11 月 23 日,id Software 延续了这一传统,公开了其上一代引擎的源代码。这份源代码被许多开发者研究过;例如,下面是 Fabien 的评价(原文):
《Doom 3 BFG》是用 C++ 编写的,这门语言如此庞大,既可以用来生成伟大的代码,也可以造出让你"辣眼睛"的怪物。幸运的是,id Software 选择了一个接近"带类的 C"(C with Classes) 的 C++ 子集,读起来毫不费劲:
- 不使用异常。
- 不使用引用(使用指针)。
- 模板的使用极少。
- 到处使用 const。
- 类。
- 多态。
- 继承。
如今许多 C++ 专家不再推荐"带类的 C"这种做法。不过,Doom3 是在 2000 到 2004 年间开发的,这或许可以解释为什么没有使用现代 C++ 机制。
让我们使用CppDepend来探索它的源代码,看看它究竟有何特别之处。
Doom3 使用少数几个项目进行了模块化;下面是它的项目列表以及关于其类型的一些统计:

下面是展示它们之间关系的依赖图:

Doom3 定义了许多全局函数。不过,大部分处理逻辑是在类中实现的。
数据模型使用结构体定义。为了具体了解源代码中结构体的使用情况,上面的度量视图将它们显示为蓝色矩形。
在度量视图中,代码库通过矩形树图(Treemap)表示。矩形树图是一种使用嵌套矩形来展示树状结构数据的方法。所用的树结构就是常见的代码层级:
- 项目包含命名空间。
- 命名空间包含类型。
- 类型包含方法和字段。

我们可以看到,代码中定义了许多结构体;例如,DoomDLL 中超过 40% 的类型是结构体。它们被系统地用于定义数据模型。许多项目都采用了这种做法,但在多线程应用程序的情况下,这种方法有一个很大的缺点:事实上,带有公有字段的结构体不是不可变的。
使用不可变对象有一个重要理由:它能极大简化并发编程。想一想——为什么编写正确的多线程代码是一项艰巨的任务?因为很难同步多个线程对资源(对象或其他操作系统资源)的访问。为什么难以同步这些访问?因为很难保证多个线程对多个对象进行的多次读写访问之间不会发生竞态条件。如果不再有写访问呢?换句话说,如果线程访问的对象的状态不再改变呢?那就不再需要同步了!
让我们搜索至少拥有一个基类的类:

将近 40% 的结构体和类拥有基类。一般来说,在面向对象编程中,继承的好处之一是多态;下面是源代码中定义的虚方法,以蓝色显示:

超过 30% 的方法是虚方法。其中纯虚方法很少,下面是定义的所有抽象类的列表:

仅定义了 52 个抽象类;其中 35 个被定义为纯接口,即它们所有的虚方法都是纯虚的。

让我们搜索使用 RTTI 的方法。

很少有方法使用 RTTI。
总结一下:这里只使用了基本的 OOP 概念——没有高级设计模式,没有滥用接口和抽象类,RTTI 的使用有限,数据以结构体定义。
到目前为止,没有什么特别之处能把这份代码与许多其他使用"带类的 C"的代码区分开来——这种做法受到许多 C++ 开发者的批评。
下面是它的开发者做出的一些有趣选择,可以帮助我们理解它的秘诀:
1. 提供一个带有实用服务的公共基类。
许多类都继承自 idClass:

idClass 提供以下服务:
- 实例创建。
- 类型信息管理。
- 事件管理。

2. 让字符串操作变得简单
一般来说,字符串是项目中使用最多的类型;许多操作都借助字符串完成,我们需要函数来操作它们。
Doom3 定义了 idStr 类,它几乎包含了操作字符串所需的所有实用方法——无需像许多其他框架提供的字符串类那样,再去定义自己的方法。
3. 源代码与 GUI 框架(MFC)高度解耦
在许多使用 MFC 的项目中,代码与其类型高度耦合,你可以在代码的任何地方找到 MFC 类型。
在 Doom3 中,代码与 MFC 高度解耦;只有 GUI 类直接依赖它,如下面的 CQLinq 查询所示:

这一选择对生产力影响很大。事实上,只有 GUI 开发者需要关心 MFC 框架;其他开发者不必在 MFC 上浪费时间。
4. 它提供了一个非常出色的工具库(idlib)
在几乎所有项目中,使用最多的类型都是工具类,如下面的查询结果所示:

我们可以看到,使用最多的是工具类。如果 C++ 开发者没有一个好用的工具框架,他们会把大部分开发时间花在与技术层的缠斗上。
idlib 提供了实用的类,包含处理字符串、容器和内存所需的全部方法,这让开发者的工作更加轻松,让他们能够更专注于游戏逻辑。
5. 实现非常容易理解
Doom3 实现了一个硬编码的编译器,正如 C++ 开发者所知,开发解析器和编译器并非易事。然而,Doom3 的实现非常容易理解,代码也非常整洁。
下面是编译器所用类的依赖图:

下面是编译器源代码中的一段代码片段:

我们已经研究过许多解析器和编译器的源代码。但这是我们第一次遇到源代码如此易于理解的编译器,整个 Doom3 源代码也是如此。这简直神奇。当我们浏览 Doom3 的源代码时,我们只能说:哇,太漂亮了!
尽管 Doom3 的设计选择非常基础,但它的设计者做出了许多决策,让开发者能够更专注于游戏逻辑,并简化所有技术层的工作,这极大地提高了生产力。
不过,在使用"带类的 C"时,你必须清楚地知道自己在做什么。你必须像 Doom3 的开发者那样成为专家。初学者不应冒险无视现代 C++ 的建议。
