读《代码大全》

2017-03-18 kk 更多博文 » 博客 » GitHub »

原文链接 http://www.kkblog.me/notes/%E8%AF%BB-%E4%BB%A3%E7%A0%81%E5%A4%A7%E5%85%A8
注:以下为加速网络访问所做的原文缓存,经过重新格式化,可能存在格式方面的问题,或偶有遗漏信息,请以原文为准。


其实我是一个月前,也就是寒假的时候就读完了这本书,受益匪浅。当时就想写篇博客总结一下,结果一直 拖到今天,实在是惭愧!

《代码大全》是一本非常厚实的书,总共有八百多页,读完用了一个月时间,大概每天读1~2章。 这本书从变量命名,循环语句,到代码质量,团队协作等等,对涉及到写代码的方方面面都做了讨论, 结合学术界研究和业界实践,告诉你什么是好代码,以及如何写出好代码。

列举一些记忆犹新的内容。

你在浪潮中的位置

在成熟的环境下--浪潮的末尾,我们有大量的编程语言可供选择,有完善的错误检查工具,强大的调试 工具和性能优化工具,文档,书籍,文章也非常丰富。

在技术浪潮的前期,可选的编程语言很少,而这些语言往往还有很多 bug,文档不完善,能用的调试工具也 往往很原始,大多数时间花在弄清楚语言如何工作,以及绕过各种 bug。

”你如何面对自己的编程工作”,取决于你在浪潮中的位置。如果你处在浪潮的前期,可以预期你将要花费 大量的时间,用来找出文档中未加说明的特性,调试各种库代码缺陷带来的错误。

子程序和伪代码编程

抛开计算机本身,子程序也算得上是计算机科学中一项最为重大的发明了。子程序的使用使得程序变得 更加易读,更易于理解,比任何编程语言的任何功能特性都更容易。

子程序可以理解为C语言中的函数或Java中的方法。它可以降低复杂度,引入中间的易懂的抽象,避免 代码重复...使用它几乎总是会带来好处。

创建子程序有很多合理的原因,但完成它的方式却有对错之分。 我们要创建高质量的子程序:内聚性,取个好名字,恰当的参数,恰当的长度。

“伪代码”是指一段用来描述算法或程序的工作逻辑的文字,用自然语言描述特定的操作。 在意图的层面上编写伪代码,用伪代码去描述解决问题的方法的意图而不是具体实现。

一段好的伪代码示例:

Keep track of current number of resources in use
If another resource is available
    Allocate a dialog box structure
    If a dialog box structure could be allocated
        Note that one more resource is in use
        Store the resource number
    Endif
Endif
Return true if a new resource was created; else return false

伪代码编程过程便是将伪代码翻译成子程序,这些子程序最后组成一个完整的较为复杂的程序。

变量的跨度和存活时间

变量只在某个范围内可引用,这个范围称为变量的作用域。 跨度是一个变量的不同引用点之间所跨越的代码行数。 存活时间是一个变量存在期间所跨越的代码总行数。

烂代码示例:

MarketingData marketingData;
SalesData salesData;
TravelData travelData;

marketingData.ComputeQuarterly();
salesData.ComputeQuarterly();
travelData.ComputeQuarterly();

marketingData.ComputeAnnual();
salesData.ComputeAnnual();
travelData.ComputeAnnual();

marketingData.Print();
salesData.Print();
travelData.Print();

其中 marketingData 有 3 次引用,分别跨了 3 行,平均跨度是 3。第一次出现到最后一次引用 之间跨了 11 行,存活时间是 11。

一张图说明变量跨度和存活时间: 变量跨度和存活时间

那些介于同一变量的多个引用点之间的代码可称为“攻击窗口”,可能会有新代码加到这种窗口中, 不当地修改了这个变量。

当把变量的引用点集中在一起的时候,也就使得代码阅读者能每次只关注一部分代码。而如果这些引用点 之间的距离非常远,就会迫使阅读者的目光在程序里跳来跳去。

因此,减小变量的跨度和存活时间,都能降低出错的可能性,并提高代码可读性。

改进后的代码:

MarketingData marketingData;
marketingData.ComputeQuarterly();
marketingData.ComputeAnnual();
marketingData.Print();

SalesData salesData;
salesData.ComputeQuarterly();
salesData.ComputeAnnual();
salesData.Print();

TravelData travelData;
travelData.ComputeQuarterly();
travelData.ComputeAnnual();
travelData.Print();

改进后,平均跨度是 0,存活时间是 2。

驯服复杂的语句

带退出的循环

带退出的循环指的是终止条件出现在循环中间而不是开始或者末尾的循环。

有重复的代码,这在维护时容易出问题:

// Compute scores and ratings.
score = 0;
GetNextRating( &ratingIncrement );
rating = rating + ratingIncrement;
while ( score < targetScore && ratingIncrement != 0 ) {
    GetNextScore( &scoreIncrement );
    score = score + scoreIncrement;
    GetNextRating( &ratingIncrement );
    rating = rating + ratingIncrement;
}

改成带退出的循环:

// Compute scores and ratings.
score = 0;
while( true ) {
    GetNextRating( &ratingIncrement );
    rating = rating + ratingIncrement;

    if ( !( score < targetScore && ratingIncrement != 0 ) ) {
        break;
    }

    GetNextScore( &scoreIncrement );
    score = score + scoreIncrement;
}

表驱动法

表驱动法是一种编程模式--从表中查找信息而不使用逻辑语句( if 和 case )。在适当的环境下, 采用表驱动法,写出的代码会比复杂的逻辑代码更简单,更容易修改。

示例,确定各月天数的笨方法:

if month == 1:
    days = 31
elif month == 2:
    days = 28
elif month == 3:
    days = 31
...

优雅做法,表驱动法:

DAYS_PER_MONTH = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]
days = DAYS_PER_MONTH[month-1]

如果考虑闰年,那长长的 if 语句将会变得更加复杂!
表驱动法依旧简单清晰:

DAYS_PER_MONTH = {
    False: [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31],
    True: [31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31],
}
days = DAYS_PER_MONTH[is_leap_year][month-1]

代码质量

系统设计检查,创建原型,代码审查,单元测试,集成测试等众多提高代码质量的方法,单独使用任何一个 方法,错误检出率都不高,平均在 40% 左右。一个有效的软件质量项目的底线,必须包括在开发的所有 阶段联合使用多种技术。下面是一套推荐阵容:

  • 对所有的需求,架构以及系统关键部分的设计进行正式检查
  • 建模或者创建原型(快速迭代)
  • 代码阅读或者检查(Lint, Code Review)
  • 执行测试(单元测试,持续集成)

性能优化

  • 8/2法则,20% 的程序耗费了 80% 的执行时间。
  • 编写可读性良好的代码,从而使程序易于理解和修改
  • 如果性能有问题,先进行分析和精确测量,找出热点
  • 对热点代码进行修改,每次调整后都对性能提升进行测量
  • 如果性能没有提升,回退到上一步(超过一半的修改都只能稍微改善性能甚至降低性能)

深入一门语言去编程

不要将编程思路局限于所用语言能自动支持的范围。杰出的程序员会考虑他们要干什么,然后才是怎样用 手头的工具去实现他们的目标。

如果某个类的子程序成员与类的抽象不一致,你会为图省事用它,而不用更一致的子程序吗? 应以尽量保持类接口抽象的方式写代码。不必因为语言支持全局数据和goto,就使用它们。 要避免用这些有危险的编程特性,取而代之以编程规范来弥补语言的弱项。 编程要使用所用语言里显而易见的方式。这等于说是"如果Freddie从桥上跳下来,难得你也愿意跳吗?"

你的语言不支持断言?那就编写自己的 assert() 子程序,也许功能上与内置的 assert() 不完全 一样,但你仍能实现其大部分用处。你的语言不支持枚举类型或具名变量?不碍事,可以按照一定方式用 全局变量定义自己的枚举或具名常量,只要有清楚的命名规范。

在一些极端情况下,特别是在新技术环境中,工具也许会原始到你不得不对所期望的编程方法有重大改变。 这时,所用语言可能使你难以采用自己期望的方法,这时你可能不得不在愿望与方法之间求得某种折中。 但即便是这种情况,仍能受益于编程规范,利用它帮助你理清环境中的危险特性。更常见的情况是, 你想做的事与工具的稳定支持差距不大,你只需对环境做出一些较小让步即可。

最后

这篇博客中的内容只是《代码大全》的冰山一角,强烈推荐你阅读《代码大全》!