《代码大全2》读书笔记
这本书的核心思想是:
-
管理软件项目的本质是管理复杂性。
-
代码承载的是人与人之间的交流。
第一部分 打好基础
第2章 隐喻
重要的研发成果常常产自类比(analogy)。通过把你不太理解的东西和一些你较为理解、且十分类似的东西做比较,你可以对这些不太理解的东西产生深刻的理解。这种使用隐喻的方法叫做“建模”。
目前最合适隐喻:建造软件(Building Software)
第3章 前期准备( Measure Twice, Cut Once)
测试只是完整的质量保证策略的一部分,而且不是最有影响的部分。
迭代开发往往能够减少“前期准备不足”造成的负面影响,但它不能完全消除此影响。
如果需求变更过于频繁,就建立一套变更控制程序(流程)。
错误处理
架构中应该清楚地说明一种“一致地处理错误”的策略。监测到一个错误时,应该“说”出来。
过度工程
在软件中,链条的强度不是取决于最薄弱的一环,而是等于所有薄弱环节的乘积。
变更策略
架构应当清楚地描述处理变更的策略。架构应该列出已经考虑过的有可能会有所增强的功能,并说明“最有可能增强的功能同样也是最容易实现的”。
质量
你不应该担心架构的任何部分。架构不应该包含任何仅仅为了取悦老板的东西。它不应该包含任何对你而言很难理解的东西。你就是那个实现架构的人;如果自己都弄不懂,那怎么实现它?
第2部分 创建高质量的代码
第5章 软件构建中的设计(Design in Construction)
好的高层次设计能提供一个可以稳妥容纳多个较低层次设计的结构。
设计是一个“Wicked Problem” – 你必须把这个问题“解决”一遍以便能够明确地定义它,然后再次解决该问题,从而形成一个可行的方案。
软件设计的最重要目的是管理复杂度(Managing Complexity)。有两类复杂度:
-
本质的(essential):一件事物必须具备,不具备就不再是该事物的属性。比如业务逻辑。
-
偶然的(accidental):碰巧具有的属性。比如集成环境,编程工具等等。
大脑没法装下整个程序。好的设计,让人在一个时刻可以只专注于一个特定的部分。
你可以把它想做是一种心理上的杂耍(边抛边接:通过轮流抛接,使两个或两个以上的物体同时保持在空中) – 程序要求你在空中(精神上)保持的球越多,你就越可能漏掉其中的某一个,从而导致设计或编码的错误。
理想的设计特征(Desirable Characteristics of a Design)
-
最小的复杂度(Minimal Complexity)。要避免做出“聪明的”设计。因为“聪明的”设计往往都是难以理解的。如果你设计的方案不能让你在专注于程序的一部分时安心地忽视其他部分的话,这一设计就没什么作用了。
-
易于维护(Ease of maintenance)
-
松散耦合(loose coupling)
-
可扩展性(extensibility)
-
可重用性(reusability)
-
高扇入(high fan-in)。高扇入就是说让大量的类使用某个给定的类。这意味着设计出的系统很好地利用了在较低层次上的工具类。
-
低扇出(low fan-out)。低扇出就是说让一个类里少量或适中(小于7个)地使用其他的类。
-
可移植性(portability)
-
精简性(leanness)。伏尔泰曾说,一本书的完成,不在它不能加入任何内容的时候,而在不能删去任何内容的时候。
-
层次性(stratification)。假设你正在编写一个新系统,其中用到很多设计不佳的旧代码,这时你就应该为新系统编写一个负责同老代码交互的层。在设计这一层时,要让它能隐藏旧代码的低劣质量,同时为新的层次提供一组一致的服务。
-
标准技术(Standard techniques)
优秀设计师的一项重要特质就是对变化的预期能力。好的程序把变化所带来的影响限制在一个子程序、类或者包的内部。
测试友好的设计,往往是好的设计。
“你在应用某种设计方法时越教条化,你所能解决的现实问题就越少”。请把设计看成是一个险恶的、杂乱的和启发式的过程,不要停留于你所想到的第一套解决方案,而是去寻找合作,探求简洁性,在需要的时候做出原型,迭代,并进一步迭代。你将对自己的设计成果感到满意。
第6章 可以工作的类(Working Classes)
警惕有超过约7个数据成员的类。
优先使用“深层副本(deep copies)”,除非论证可行,才采用“浅层副本(shallow copies)。
第7章 高质量的子程序(High-Quality Routinges)
复杂的布尔判断,应该使用子程序来实现。
人类很难同时记住超过7个单位的信息。
子程序的名字是它的质量的指示器。如果名字糟糕但恰如其分,那说明这个子程序设计得很差劲。如果名字糟糕而且又不准确,那么它就反映不出程序是干什么的。不管怎样,糟糕的名字都意味着程序需要修改。
第8章 防御式编程(Defensive Programming)
保护程序免遭非法输入数据的破坏。检查所有来源于外部的数据的值。
用错误处理代码来处理预期会发生的状况,用断言来处理绝不应该发生的状况。
可以让软件的某些部分(某个抽象层,公用方法等)处理“不干净的”数据,而让另一些部分处理“干净的”数据,即可让大部分代码无须再担负检查错误数据的职责。
异常
用异常通知程序的其他部分,发生了不可忽略的错误。在恰当的抽象层次抛出恰当的异常,比如底层异常不应该在高层抽象中被抛出。
第3部分 变量
第10章 使用变量的一般事项(General Issues in Using Avriables)
初始化
在声明变量的时候初始化。
理想情况下,在靠近第一次使用变量的位置声明和定义改变量。
在可能的情况下使用final或者const。
作用域
使变量的引用局部化,减小变量的作用域:
-
在循环开始之前再去初始化该循环里使用的变量,而不是在该循环所属的子程序的开始处初始化这些变量。
-
直到变量即将被使用时再为其赋值。
-
把相关语句放在一起,可能的情况下提取成单独的子程序。
-
开始时采用最严格的可见性(比如设为private),然后根据需要扩展变量的作用域。
“方便性”和“智力可管理性”两种理念之间的区别,归根结底来源于侧重写程序还是读程序之间的区别。
每个变量只应该用于单一用途,且不应该有隐藏含义(比如当x大于5000时代表什么)。
第11章 变量名的力量(The Power of Variable Names)
通常,对变量的描述就是最好的变量名。名字对于代码读者的意义要比对作者更重要。
一个好名字通常表达的是“what”而不是“how”。一般而言,如果一个名字反映了计算的某些方面而不是问题本身,那么它反映的就是“how”而不是“what”了。
较长的名字适用于较长的作用域,较短的名字适用于短的作用域(循环、小的方法等)。
计算限定词,如Total、Sum、Average、Min、Max、String、Pointer等等,应该放在名字的最后。
典型的布尔变量名:done, error, found, success, ok等。布尔变量应该是那些隐含了“真/假”含义的名字,如done和success等。应该使用肯定的布尔变量命名,而不是notFound,notDone等。
枚举类型要有统一的组前缀。
避免使用具有相似含义的名字。如果你能够交换两个变量的名字而不会妨碍对程序的理解,那么你就需要为这两个变量重新命名了。
命名规则应该能够区分局部数据、类的数据和全局数据。
第12章 基本数据类型(Fundamental Data Types)
Tips:
-
避免使用Magic Number。
-
每次遇到除法时都要检查预防除零错误。
-
显式地使用类型转换。
-
避免混合类型的比较。
-
整数:小心溢出。
-
浮点数:避免“相等”判断,避免数量级相差巨大的数之间的加减运算。
-
字符和字符串:尽早决定是否使用Unicode
-
C语言中的字符串:
-
警惕任何包含字符串和等号的表达式
-
字符串的长度声明为“CONSTANT+1”
-
用null初始化字符串以避免没有结束符的字符串,使用
calloc()
而不是malloc()
-
-
布尔变量:使用布尔变量来简化复杂的判断
-
数组:
-
任何情况下,确认所有的数组下标都没有超出数组的边界
-
尽量使用容器来取代数组,或者将数组作为顺序化结构来处理
-
-
自定义类型
-
使用自定义类型(通过typedef等方法)实现更好的抽象
-
给所创建的类型取功能导向的名字,避免使用那些代表了计算机数据类的类型名(如BigIngeger
-
, LongString等)。
第13章 不常见的数据类型(Unusual Data Types)
-
指针:
-
把指针的操作限制在子程序或类里面。比如通过
NextLink()
,PreviousLink()
等方法代替直接操作指针。 -
指针的声明和定义放在一起(一行)。
-
在与“指针分配”相同的作用域中删除指针。
-
在使用指针之前检查指针。
-
用额外的指针变量来提高代码清晰度,避免类似
pointer->next->last->next
等使用指针的方法 -
在删除或者释放指针之后把它们设为空值(null)
-
-
全局数据:
-
仅在确实需要的使用使用全局数据(首先把每一个变量设置为局部的,仅当需要是再设置为全局的)
-
全局数据应该让人一看便知,如果使用了全局数据,就公开地使用
-
用访问器子程序(Access Routines)来取代全局数据
-
根据全局数据的功能,把全局数据分为不同的模块(包、类)
-
用
static
关键字或者它的等价物来声明该数据,确保该数据只有单一实例 -
确保对一项数据的所有访问都发生在同一个抽象层上
-
-
第14章 组织直线型代码(Organizing Straight-Line Code)
依赖关系必须清晰明显。通子程序名、程序参数等实现。
第15章 使用条件语句(Using Conditionals)
If-ElseIf-Else结构中,最后一个Else确保所有的情况都考虑到了。
Case语句的顺序:首先按照频率从高到低,频率一样按照字母排列。
第16章 控制循环(Controlling Loops)
使用带退出的循环,类似:
while True:
## do something
if some_condition:
break
## do something
把初始化代码紧放在循环前面。
在循环的开始处用continue进行判断。
一个循环只做一件事。如果用两个循环会导致效率地下,而使用一个循环很合适,那么就把代码写成两个循环,并注明可以把它们合并起来以提高效率,然后等测量数据显示程序的这一部分性能低下的时候再去合并它们。
避免出现依赖循环下标最终取值的代码。
小心那些有很多break散布其中的循环。一个循环包含很多的break,有可能意味着程序员对该循环的结构或者对循环本身的角色缺乏清晰的认识。在大量使用break的场合中,用一系列的循环而非一个含有多个出口的循环可能会使表达更清晰。
循环要尽可能短,嵌套限制在3层内。
第17章 不常见的控制结构(Unusual Control Structures)
子程序中的多处返回(Multiple Returns):如果能增强可读性,那么就使用return。
小心谨慎地使用递归。
第18章 表驱动法(Table-Driven Methods)
使用一目了然的表来代替复杂的逻辑判断。
第19章 一般控制问题(General Control Issues)
Tips:
-
编写肯定形式的布尔表达式
-
按照数轴的顺序编写数值表达式,例如
MIN_ELEMENTS <= i and i <= MAX_ELEMENTS
-
与0比较的指导原则:
-
隐式地比较逻辑变量
-
把数和0相比较,使用
while (balance != 0) ...
而不要使用while (balance) ...
-
在C语言中显式地比较字符和
\0
-
把指针与NULL相比较,使用
while (p != NULL) ...
而不要用while (p)
-
控制结构域复杂度密切相关。人类大脑很难处理好超过5到9个的智力实体。
第5部分 代码改善(Code Improvement)
第20章 软件质量概述(The Software-Quality Lanscape)
质量不应该被认为是次要目标。组织本身必须向程序员们说明,质量应当放在第一位。
实现软件质量目标的拦路虎之一就是失控的变更。
阅读代码是最有效率的找出缺陷的方法。
没有任何一种错误检测方法能够解决全部问题,测试本身并不是排除错误的最有效方法。成功的质量保证计划应该使用多种不同的技术来检查各种不同类型的错误。
缺陷可能在任何阶段渗透到软件中。因此,你需要在早期阶段就开始强调质量保证工作,并且将其贯彻到项目的余下部分中。质量保证工作应该作为技术脉络的一部分,以及项目的结束点。
提高生产效率和改善质量的最佳途径就是减少花在这种代码返工上的时间,无论返工的代码是由需求,设计改变还是调试引起的。
更多的质量保证工作能降低错误率,但不会增加开发的总成本。编写无缺陷软件并不一定会比编写富含缺陷的软件花更多的时间。开发高质量代码最终并没有要求你付出更多,只是你需要对资源进行重新分配,以低廉的成本来防止缺陷的出现,从而避免代驾高昂的修正工作。
第21章 协同构建(Collaborative Construction)
协同开发实践往往能比测试发现更多的缺陷,并且更有效。
协同开发实践所发现的错误的类型通常跟测试所发现的不同,这意味着你需要同时使用详查和测试来保证你软件的质量。
对代码进行详细检查是减少缺陷的高效方法。
代码详查的记过不应当作为员工表现的评估,因为详查中的代码任然属于开发阶段,评估的依据应该是最终产品而非尚未完成的工作。
进行详查的目的是发现设计或代码总的缺陷,而不是探索替代方案,或者争论谁对谁错,其目的绝不应该是批评作者的设计或者代码。对所有人来说,这一过程都应该是正面的:程序得到了明显改善,参与者都学到了一些东西。
对代码的非正式检查(走查)效果并不好。
软件质量的普遍原理:在减少软件中的缺陷数量的同时,开发周期也能得到缩短。
结对编程的关键(Keys to Success with Pair Programming)
-
统一编码规范
-
不要让结对编程变成旁观
-
不要强迫在简单的问题上使用结对编程
-
有规律地对结对人员和分配的工作任务进行轮换
-
鼓励双方跟上对方的步伐
-
避免新手组合
-
指定一个组长
第22章 开发者测试(Developer Testing)
-
错误并非平均地分布在所有的子程序里面,二是集中在少数几个子程序里面。
-
大多数错误的影响范围是相当有限的(不超过一个子程序)。
-
大多数的构建期的错误是编人员的失误造成的。
-
笔误(拼写错误)是一个常见的问题根源
-
很多错误是由于没有彻底地理解设计
软件质量的普遍原则:开发高质量的软件,比开发低质量软件然后修正的成本要低廉。
第24章 重构(Refactoring)
重构策略:
-
在增加子程序、类时进行重构
-
在修补缺陷时进行重构
-
关注容易出错的模块
-
关注高度复杂的模块
-
如果在维护一个系统,改善你手中正在处理的代码。确保代码在离开你的时候比来之前更健康。
-
定义清楚干净代码和拙劣代码之间的边界,然后尝试把代码移过这条边界。
第6部分 系统考虑(System Considerations)
第27章 程序规模对构建的影响(How Program Size Affects Construction)
随着项目规模的增大,更大一部分错误要归咎于需求和设计。
随着项目规模的增长,构建活动将只占项目总工作量的一小部分。
对于大项目来说,如果不有意识地去选择方法论,就将无法完成任务。
项目越正规,你不得不写的文件的数量也越来越多,用于确认你已经完成了自己的工作。
随着项目规模的扩大,交流需要加以支持。大多数方法论的关键点都在于减少交流中的问题,而一项方法论的存亡关键也应取决于它能否促进交流。
在其他条件都相等的时候,大项目的生产率会低于小项目。而大项目的每千行代码错误会高于小项目。
在小项目里的一些看起来“理当如此”的活动在大项目中必须仔细地计划。随着项目规模扩大,构建活动的主导地位逐渐降低。
放大轻量级方法论要好于缩小重量级方法论。最有效的办法是使用“适量级”方法论。
第28章 管理构建(Managing Construction)
鼓励良好编码实践的一些技术:
-
给项目每一部分分派两个人。
-
逐行复查代码。
-
要求代码签名。
-
安排一些好的代码示例供人参考。
-
强调代码是共有财产。
一个简单而有效的衡量标准是:“我必须能阅读并理解这个项目里的所有代码”。
需求变更和设计变更:
-
遵循某种系统化的变更控制手续。
-
成组地处理变更请求。记下所有的想法和建议,不管它实现起来有多容易。把它记录下来,直到你有时间取处理它们。到那时,把它当做整体来看待,从中选中最有益的一些变更来加以实施。
进度落后时,增加时间通常并不可行。
任何一种项目特征都是可以用某种方法来度量的,而且总会比根本不度量好得多。度量结果也许不会完全精确,度量也许很难做,而且也需要持续不断地改善结果,但是它能使你对软件开发过程进行控制,而如果不度量就根本不可能控制。
程序员之间有着数量级的差异。即时个体程序员都一样,不同编程团队在软件质量和生产率上也有着相当大的差异。
第29章 集成(Integration)
要点:
-
构建的先后顺序和集成的步骤会影响涉及、编码、测试各类的顺序。
-
一个经过充分思考的集成顺序能减少测试的工作量,并使调试变容易。
-
增量集成有若干变型,而且–除非项目是微不足道的–任何一种形式的增量集成都比阶段式集成好。
-
针对每个特定的项目,最佳的集成步骤通常是自顶向下、自底向上、风险导向以及其他集成方法的某种组合。T型集成和竖直分块集成通常都能工作得很好。
-
Daily Build能减少集成的问题,提升开发人员的士气,并提供非常有用的项目管理信息。
第7部分 软件工艺(Software Craftsmanship)
第32章 自说明代码(Self-Documenting Code)
好代码本身就是最好的说明。如果代码太糟,需要大量注释,应先试着改进代码,直至无须过多注释为止。
注释应说出代码无法说出的东西–例如概述或用意等信息。
第33章 个人性格(Personal Character)
编程工作本质上是项无法监督的工作,因为没人真正清楚你正在做什么。
高智商与优秀的程序员之间并无太密切的联系。
编程首先是与人交流,其次才是与计算机交流。
人的个性对其编程能力有直接影响。
最有关系的性格为:谦虚、求知欲、诚实、创造性、纪律,以及高明的偷懒。
小聪明、经验、坚持和疯狂既有助也有害。
好性格与培养正确的习惯关系甚大。要成为杰出的程序员,先要养成良好习惯、其他自然水到渠成。