《代码大全》(Code Complete)书摘

上次更新时间:2016年1月20日


应该首先为人编写代码,其次才是为机器。

表驱动法:将复杂的逻辑判断转换为查表,从而简化代码的编写与维护。

防御式编程存在的理由:应对严酷的充斥非法数据的真实世界。

许多强大的编程技术在被编程领域的大众接触之前,都已在学术论文和期刊界尘封了多年。

对于需求,人们可以自以为是而不去潜心分析;对于架构,人们可以偷工减料而不去精心设计;对于测试,人们可以短斤少两甚至跳过不做,而不去整体计划并付诸实施。

借助隐喻,可以更深刻地理解软件开发的过程。

对于编程来说,最大的挑战还是将问题概念化。

使用隐喻的方式:用它来提高你对编程问题和编程过程的洞察力,帮助你思考编程过程中的活动,想象出更好的做事情的方法。

《人月神话》

从管理的角度看,做计划意味着确定项目所需要的时间、人数以及计算机台数。从技术角度讲,做计划意味着弄清楚你想要建造的是什么以防止浪费钱去建造错误的东西。

建造软件系统跟其他任何花费人力财力的项目是相似的。

做软件时,必须按正确的顺序去做事情。

程序员是软件食物链的最后一环。架构师吃掉需求,设计师吃掉架构,而程序员则消化设计。

发现错误的时间要尽可能接近引入该错误的时间。

开发商业系统的项目往往受益于高度迭代的开发法,性命攸关的系统往往要求采用更加序列式的方法。

对于绝大多数项目来说,尽早把哪些是最关键的需求要素和架构要素确定下来是很有价值的。

问题定义应该用客户的语言来书写,而且应该从客户的角度来描述问题。

最好的解决方案未必是一个计算机程序。

如果没有良好的问题定义,你努力解决的问题可能是一个错误的问题。

要求一套明确的需求,这点很重要,理由很多。

如果你的需求不够好,那么就停止工作,退回去,先把它做好,再继续前进。

有些需求作为功能特色来看是不错的想法,但是当你评估“增加的商业价值”时就会觉得它是个糟糕透了的主意。那些记得“考虑自己的决定所带来的商业影响”的程序员的身价与黄金相当。

功能需求核对(节选):
是否详细定义了系统的全部输入,包括其来源、精度、取值范围、出现频率等?

需求的品质(节选):
是否避免在需求中规定设计(方案)?
是否每条需求都是可测试的?是否可能进行独立的测试,以检查满不满足各项需求?
是否详细描述了所有可能的对需求的改动,包括各项改动的可能性?

架构的质量决定了系统的“概念完整性”。

应该明确定义各个构造块的责任。每个构造块应该负责某一个区域的事情,并且对其他构造块负责的区域知道的越少越好。通过使各个构造块对其他构造块的了解达到最小,能将设计的信息局限于各个构造块之内。

是否将用户界面模块化,使界面的变更不会影响程序其余部分。
架构应该使我们很容易地做到:砍掉交互式界面的类,插入一组命令行的类。

稀缺资源包括数据库连接、线程、句柄(handle)等。

错误处理:
在程序中,在什么层次上处理错误?你可以在发现错误的地方处理,可以将错误传递到专门处理错误的类进行处理,或者沿着函数调用链往上传递错误。

系统使用某个不会对系统其余部分产生危害的虚假值(phony value)代替错误值。

架构也许规定使用表驱动(table-driven)技术(而不使用硬编码的if语句)。它也许还规定“表”中的数据是保存在外部文件中,而非直接写在程序代码中,这样就能做到在不重新编译的情况下修改程序。
151223注:自己已悟出并热衷于使用这个技术。

架构应该是带有少许特别附加物的精炼且完整的概念体系。
《人月神话》的中心论题,说的就是大型系统的本质问题是维持其“概念完整性”。
在查看架构的时候,你应该很愉快,因为它给出的解决方案看上去既自然又容易。

顶层设计是否独立于用作实现它的机器和语言?
优秀的软件架构很大程度上是与机器和编程语言无关的。

避免提前去做那些放到构建设计期间能做的更好的工作。

在弄清楚要做的是什么之前,没人会相信你能估算出合理的进度表。

一套好的符号系统能把大脑从所有非必要的工作中解脱出来,集中精力去对付更高级的问题。
151223注:这或许就是抽象的强大力量。数学本身是高度的抽象,只是现在还没有一个合适的机会认识并爱上数学符号体系的强大和美丽。

Ada是一种通用的高级编程语言,基于Pascal。

Cobol是一种像英语的编程语言,主要适用于商业应用。

Fortran代表“FORmula TRANslation”。

Java设计为能在任何平台上运行,办法是将Java源码转变为字节码(byte code),然后让后者在各个平台上的虚拟机环境中运行。Java广泛用于Web应用的编程。

Perl是一种处理字符串的语言,基于C和若干UNIX工具程序。Perl常用于系统管理任务,诸如建立生成脚本(build script),也用于生成及处理报表。也可用来创建Web应用程序,例如Slashdot。Perl是“Practical Extraction and Report Language(实用摘要及报告语言)”的首字母缩写。

PHP能在所有主要的操作系统上运行,用来执行服务器端的交互功能。

SQL是“声明式”语言,意思是说,它不是定义一系列操作,而是定义某些操作的结果。

大多数重要的编程原则并不依赖特定的语言,而依赖于你使用语言的方式。

你有没有确定,多少设计工作将要预先进行,多少设计工作在键盘上进行(在编写代码的同时)?
151229注:最近倾向于边设计边实现,其中,设计的含义包括构思想法,绘制概念图等。

“险恶的(wicked)”问题就是那种只有通过解决或部分解决才能被明确的问题。
151229注:例如,人脑工作机制的研究,或许只有人工智能发展到一定程度之后,才真正会有所突破。

设计就是确定取舍和调整顺序的过程。

把设计的特性综合归纳起来,可以说设计是“自然而然形成的”。设计不是在谁的头脑中直接跳出来的。它是在不断的设计评估、非正式讨论、写试验代码以及修改试验代码中演化和完善的。

管理复杂度是软件开发中最为重要的技术话题。软件的首要技术使命就是管理复杂度,它实在是太重要了。

如果能最终抛弃或者重构旧代码,那时就不必修改除交互层之外的任何新代码。

只有当“确需了解”——最好还有合理的理由——时,才应该允许子系统之间的通信。

你看得到的就是你能——全部——得到的。

隐藏设计决策对于减少“改动所影响的代码量”而言是至关重要的。

大多数让信息无法隐藏的障碍都是由于惯用某些技术而导致的心理障碍。

请养成问“我该隐藏些什么?”的习惯,你会惊奇地发现,有很多很棘手的设计难题都会在你面前化解。

主动应对变更的最有力的技术之一,是表驱动技术。

请尽量使你创建的模块不依赖或很少依赖其他模块。

类和子程序是用于降低复杂度的首选和最重要的智力工具。

策略(Strategy)模式:定义一组算法或者行为,使得它们可以动态地相互替换。

唯一一个正确位置原则(the Principle of One Right Place):对于每一段有作用的代码,应该只有唯一的一个地方可以看到它,并且也只能在一个正确的位置上去做可能的维护性修改。

关于理解问题:
未知量是什么?现有的数据是什么?条件是什么?能够满足这些条件吗?这些条件足以决定出未知量吗?还是并不够?或者其中有冗余?甚至是矛盾的?

如何确定分解的程度呢?持续分解,直到看起来在下一层直接编码要比分解更容易。

最大的设计问题通常不是来自于那些被认为是很困难的,并且在其中做出了不好的设计的区域;而是来自于那些被认为是很简单的,而没有做出任何设计的区域。

把设计文档插入到代码里。
151229注:这风格值得尝试。

把画在白板上的图表照成相片然后嵌入到传统的文档里,这样做可以带来事半功倍的效果,因为它的工作量只是用画图工具画设计图表的1%,而它的收益却能达到保存设计图表的80%。

要想理解面向对象编程,首先要理解ADT。不懂ADT的程序员开发出来的类只是名义上的“类”而已——实际上这种“类”只不过就是把一些稍有点儿关系的数据和子程序堆在一起。然而在理解ADT之后,程序员就能写出在一开始很容易实现,日后也易于修改的类来。

类接口协调的一个表现是其抽象层次一致。

程序员使用容器类或者其他类库来实现内部逻辑时,应把“使用类库”这一事实隐藏起来。

要顶住诱惑,不到类接口的私用部分去寻找关于实现细节的线索。

不对类的使用者做出任何假设。

避免使用友元类。

警惕有超过7个数据成员的类。

如果你只是想使用一个类的实现而不是接口,那么就应该采用包含方式,而不该用继承。

完全可以通过把构造函数、赋值运算符或其他成员函数或运算符定义为private,从而禁止调用方代码访问它们。
注:把构造函数定义为private是定义单例类时所用的标准技术。

如果可能,应该在所有的构造函数中初始化所有的数据成员。

深拷贝在开发和维护方面都要比浅拷贝简单。实现浅拷贝出了要用到两种方法都需要的代码之外,还要增加许多代码用于引用计数、确保安全地复制对象、安全地比较对象以及安全地删除对象等。例如,OpenCV中的Mat类。

无论复杂度表现为何种形态——复杂的算法、大型数据集、或错综复杂的通讯协议等——都容易引发错误。

把容易变动的部分隔离开来,这样就能把变动所带来的影响限制在一个或少数几个类的范围内。把最容易变动的部分设计成最容易修改的。容易变动的部分有硬件依赖性、输入/输出、复杂数据类型、业务逻辑等。

如果需要用到全局数据,就可以把它的实现细节隐藏到某个类的接口背后。与直接使用全局数据相比,通过访问器子程序来操控全局数据有很多好处。你可以改变数据结构而无须修改程序本身。你可以监视对这些数据的访问。“使用访问器子程序”的这条纪律还会促使你去思考有关数据是否就应该是全局的;经常你会豁然开朗地发现,“全局数据”原来只是对象的数据而已。

在Java中,所有的方法默认都是可以覆盖的,方法必须被定义成final才能阻止派生类对它进行覆盖。

是否已从类中去除无关信息?

类的文档中是否记述了其继承策略?

是否在构造函数中初始化了所有的数据成员?

不可移植的部分包括编程语言所提供的非标准功能、对硬件的依赖,以及对操作系统的依赖等。

应该把复杂的布尔判断放入函数中,以提高代码的可读性。

避免使用无意义的、模糊或表述不清的动词。如,HandleCalculation、PerformService等。

在任何时候,复杂的算法总会导致更长的子程序。

按照输入-修改-输出的顺序排列参数。

可以通过预处理指令来自己创建in和out关键字:

// C++ 示例
#define IN
#define OUT
void invertMatrix( IN Matrix originalMatrix, OUT Matrix& resultMatrix );

使用C++中的const关键字来定义输入参数通常更为适宜。

应该在接口中对参数的假定加以说明:
在子程序内部和调用子程序的地方同时对所做的假定进行说明是值得的。不要等到把子程序写完之后再回过头去写注释——你是不会记住所有这些假定的。
例如,所能接受的数值的范围、不该出现的特定数值。

把子程序的参数个数限制在大约7个以内。对于人的理解力来说,7是一个神奇的数字。

考虑对参数采用某种表示输入、修改、输出的命名规则。

留意编译器给出的关于参数类型不匹配的警告。

函数是指有返回值的子程序;过程是指没有返回值的子程序。

在函数开头用一个默认值来初始化返回值是个很好的做法——这种做法能够在未正确地设置返回值时提供一张保险网。

不要返回指向局部数据的引用或指针。

可以正确展开的宏示例:

// C++ 示例
#define Cube( a ) ( ( a ) * ( a ) * ( a ) )

应节制使用inline子程序,因为C++要求程序员把inline子程序的实现代码写在头文件里,从而也就把这些实现细节暴露给了所有使用该头文件的程序员。

留意接口假定是否已在文档中说明。
注:这里的接口假定可认为是对输入数据合理范围的说明。

把断言看作是可执行的注解。

处理错误最恰当的方式要根据出现错误的软件的类别而定。

严格来说,健壮性和正确性这两个术语在程度上是截然相反的。

异常和继承有一点是相同的,即:审慎明智地使用时,它们都可以降低复杂度;而草率粗心地使用时,只会让代码变得几乎无法理解。

应该在异常消息中加入关于导致异常发生的全部信息。例如,如果异常是因为一个数组下标错误而抛出的,就应在异常消息中包含数组的上界、下界以及非法的下标值等信息。

应当了解所用函数库可能抛出的异常。未能捕获由函数库抛出的异常将会导致程序崩溃,就如同未能捕获由自己代码抛出的异常一样。

让软件的某些部分处理“不干净的”数据,而让另一些部分处理“干净的”数据,即可让大部分代码无须再担负检查错误数据的职责。

友好的错误消息:一种常用而且有效的方法,是通知用户说发生了“内部错误”,并留下可供报告该错误的电子邮件地址或电话号码。

应该避免在构造函数和析构函数中抛出异常。

在伪代码编程过程中,避免使用目标语言中的语法元素。

伪代码一经写好,就可以依照它去生成代码了,同时还把伪代码变成编程语言中的注释。
每一段注释可以产生出一行或多行代码。
注:有过这样的体验。

应该意识到子程序是应隐藏某些“丑陋”的信息的。

对于绝大多数系统而言,程序的效率并不十分紧要。

常规情况下,在每个子程序上为效率问题卖力通常是在白费功夫,最主要的优化还是在于完善高层的设计,而不是完善每个子程序。

在整个构建过程的后期才开始编译能够带来诸多好处。到你已经让自己相信一个子程序是正确时再编译它,可以避免在匆忙中完成代码。
注:已经有意识地在督促自己这样做。

可以尝试着把编译器的警告级别调到最高。

不恰当的变量初始化所导致的一系列问题都源于变量的默认初始值与你的预期不同。

在下一次使用计数器变量之前重置其值是一种常见的错误。

数据类型的隐式转换是致命的错误。
注:它曾给我造成巨大的困扰。

应尽可能缩短变量的存活时间。

开始时采用最严格的可见性,然后根据需要扩展变量的作用域。

采用越晚的绑定时间越有利。
注:我的办法是从文件中载入运行期参数。

应确保每个变量只用于单一用途。

据研究发现,当变量名的平均长度在10到16个字符的时候,调试程序所需花费的力气是最小的。

导致循环变长的常见原因之一是出现循环的嵌套使用。如果你使用了多个嵌套的循环,那么就应该给循环变量赋予更长的名字以提高可读性。

应为状态变量取一个比flag更好的名字。

在使用枚举类型的时候,可以通过使用组前缀,例如Color_,Planet_来明确表示该类型的成员都同属于一个组。

程序员们在整个项目生命周期里会把更多的时间花在阅读代码而不是编写代码上。

应避免在名字中使用数字,如果名字中的数字真的非常重要,就使用数组来代替一组单个的变量。如果数组不合适,那么数字就更不合适。

关于避免使用神秘文字量:
一条很好的经验法则是,程序主体中仅能出现的文字量就是0和1,任何其他文字量都应该换成更有描述性的表示。

务必杜绝混合类型的比较,如整型和浮点型。

杰出的程序员会修改他们的代码来消除所有的编译器警告,通过编译器警告来发现问题要比你自己找容易得多。

要避免数量级相差巨大的数之间的加减运算。
如果必须要把一系列差异如此巨大的数相加,那么就先对这些数排序,然后从最小值开始把它们加起来。

避免浮点数的等量判断。

应该用布尔变量来简化复杂的判断。

请成为从代码中剔除文字量的狂热者。

需遵循的典型的作用域原则是:优先选用局部作用域,其次是类作用域,再次是全局作用域。

ARRAY_LENGTH( )宏:
#define ARRAY_LENGTH( x ) ( sizeof( x ) / sizeof( x[0] ) )

对用户的信任要适度。

像C++这样要求你通过头文件来分发变量类型定义的语言里较难实现真正的信息隐藏。

Pascal和Ada已经在走向灭亡。

创建自定义数据类型的原则:
给所创建的类型取功能导向的名字。应该用能代表该新类型所表现的现实世界问题的类型名。

可以尽可能多地使用自己创建的类型。

你可能感兴趣的:(软件构建)