复杂性啊,愚人对你视而不见,实干家受你所累。
有些人避而远之。唯智者能够善加消除。 ——Alan Perlis
我知道,但又忘记了 Hoare 的至理名言:不成熟的优化是程序设计中的万恶之源。——Donald Dnuth,The Errors of TeX
依赖性管理是软件工程的一个基础,也是贯穿本书不断出现的主题。
任意优秀的软件工程技术,都在想尽办法减少依赖性:
- 继承?是为了代码的使用不依赖于实际派生类的基类(其实这句话我没读明白……)
- 减少全局变量?是为了避免产生大量的远距离依赖。
- 抽象?是为了消除表示概念的代码与实现功能的代码间的依赖。
- 信息隐藏?是为了客户代码不依赖于实现细节。
- ……
第 5 条:一个实体只应有一个紧凑的职责
这一点无需多言,书中给出了一个反例——realloc
。
在标准 C 语言中,realloc
是一个臭名朝著的不良设计:
- 如果传入
NULL
,它就像malloc
一样分配新的内存空间。 - 如果大小参数为 0,它就承担
free()
的任务释放内存空间。 - 如果可行,它就就地重新分配,反之就到其他地方分配。
这个函数不易扩展,人们普遍认为它是一个目光短浅的失败设计。
书中提到的另一个不良设计的例子是 std::basic_string
,一个臃肿的巨大类,这里不进行介绍了。
第 6 条:正确、简单和清晰第一
著名的 KISS 原则(Keep It Simple Software):
- 质量优于速度
- 简单优于复杂
- 清晰优于机巧
- 安全优于隐患
这是书中提到的名言之一:
程序必须为阅读它的人儿编写,只是顺便利于机器执行。——Harold Abelson 和 Gerald Jay Sussman
避免不必要/小聪明式的重载
有一个古怪的图形界面库,用 w+c
表示在图形组件 w 上添加子控件 c,这显然是不必要的。
尽量使用命名变量,而非临时变量
第 7 条:伸缩性的考量
不要进行不成熟的优化,也不要随意将程序劣化。
寻找平衡点的关键在于:集中精力改善算法的复杂度,而非拘泥于节省一次循环,一个加法等小型优化。
第 8 条:不要进行不成熟的优化
让一个正确的程序更快速,
比让一个快速的程序变得正确,
要容易得太多、太多。
现代计算机大都具有极为复杂的计算模型,经常是几个流水线处理单元并行工作,深高速缓存层次结构,猜测执行(speculative execution),分支预测……这些都还只是 CPU 层面的优化。
在诸多硬件优化之上,还有编译器的全力优化,而在这些复杂的架构之上,还有你——程序员的猜测。因此,许多的“优化”措施实际上不一定真的能发挥作用。
而且,现代的程序,它的整体性能不一定仅仅受计算性能的限制,它还受内存大小的限制,网络性能的限制,数据库连接,硬盘读写等诸多限制。仅仅在计算方面的优化,对整体性能的提升十分有限,投入宝贵的人力物力去改善这些效果并不显著的地方很不划算。
因此,应将正确性放在第一位,算法的复杂度第二,其次在考虑细枝末节的优化。
书中给出的例子是:
++
,--
的后缀运算的不当使用。- inline 的滥用。
- 纠结于引用传参与复制传参。
第 9 条:避免不成熟的劣化(pessimization)
有些地方本可以通过简单的方式达到更好的性能,却因一时方便采取低效的方式。这是与过度优化相反的另一个极端。
书中多处提到程序劣化的例子。
善用抽象与库是解决问题的方式之一。
第 10 条:尽量减少全局和共享的数据
- 全局变量造成了远距离数据间的耦合,这使程序的维护和调试更加困难。
- 全局变量还常常会污染全局命名空间。
- 全局变量的初始化顺序是未定义的,这使全局变量的初始化十分复杂。
- 请尽量用通信方式(如消息队列)代替全局变量。
第 11 条:隐藏信息
- 隐藏信息意味着接口的不变性,它限制了变化的范围,避免造成 “连锁反应” 。
- 尽量不要公开类的数据成员,或其句柄与指针。一个例外是纯粹的值的集合(如 C 中的 struct),和进行白箱测试时。
第 12 条:懂得何时、如何进行并发性编程
(我还没学会并发编程,之后会补上的……)
第 13 条:确保资源为对象所拥有,使用显示的 RAII 和智能指针
- C++ 的 “资源获取即初始化”(Resource Acquisition Is Initialization,RAII)是正确处理资源的利器。
- 获取原始资源时,应立即将其传递给所属对象。
- 一条语句内只分配一个对象的资源。
- 资源的初始化应在构造函数中进行,其销毁应在析构函数中进行。
- 实现 RAII 类时,要注意构造和赋值函数的设计,编译器生成的默认版本不一定正确,单例的资源应禁止其复制(但可以移动)。