程序员修炼手册(其一)

如何使用日志调试

Logging(日志)是一种编写系统的方式,可以产生一系列信息记录,被称为 log。Printlining 只是输出简单的,通常是临时的日志。初学者一定要理解并且使用日志,因为他们对编程的理解是局限的。因为系统的复杂性,系统架构必须理解与使用日志。在理想的状态下,程序运行时产生的日志信息数量需要是可配置的。通常,日志提供了下面三个基本的优点:

日志可以提供一些难以重现的 bug 的有效信息,比如在产品环境中发生的、不能在测试环境重现的 bug。
日志可以提供统计和与性能相关的数据,比如语句间流逝过的时间。
可配置的情况下,日志允许我们获取普通的信息,使得我们可以在不修改或重新部署代码的情况下调试以处理具体的问题。
需要输出的日志数量总是一个简约与信息量的权衡。太多的信息会使得日志变得昂贵,并且造成滚动目盲,使得发现你想要的信息变得很困难。但信息太少的话,日志可能不包含你需要的信息。出于这个原因,让日志的输出可配置是非常有用的。通常,日志中的每个记录会标记它在源代码里的位置,执行它的线程(如果可用的话),时间精度,并且通常还有一些额外的有效信息,比如一些变量的值,剩余内存大小,数据对象的数量,等等。这些日志语句撒遍源码,但只出现在主要的功能点和一些可能出现危机的代码里。每个语句可以被赋予一个等级,并且只有在系统被配置成输出相应等级的记录的时候才输出这个等级的记录。你应该设计好日志语句来标记你预期的问题。预估测量程序表现的必要性。

如果你有一个永久的日志,printling 现在可以用日志的形式来完成,并且一些调试语句可能会永久地加入日志系统。

如何优化循环

有时候你会遇到循环,或者递归函数,它们会花费很长的执行时间,可能是你的产品的瓶颈。在你尝试使循环变得快一点之前,花几分钟考虑是否有可能把它整个移除掉,有没有一个不同的算法?你可以在计算时做一些其他的事情吗?如果你不能找到一个方法去绕开它,你可以优化这个循环了。这是很简单的,move stuff out。最后,这不仅需要智慧而且需要理解每一种语句和表达式的开销。这里是一些建议:

删除浮点运算操作。
非必要时不要分配新的内存。
把常量都放在一起声明。
把 I/O 放在缓冲里做。
尽量不使用除法。
尽量不适用昂贵的类型转换。
移动指针而非重新计算索引。
这些操作的具体代价取决于你的具体系统。在一些系统中,编译器和硬件会为你做一些事情。但必须清楚,有效的代码比需要在特殊平台下理解的代码要好。

如何进行实验

已故的伟大的 Edsger Dijkstra 曾经充分解释过:计算机科学不是一门实验科学[ExpCS],并且不依赖于电子计算机。当他提出这个观点时,他指的是 19 世纪 60 年代。[Knife]

…危害已经出现:主题现在已经变成了“计算机科学” - 这实际上,像是把外科手术引用为“手术刀科学” - 这在人们心中深深植入了这样一个概念:计算机科学是关于机器和它们的外围设备的。

编程不应该是一门实验科学,但大多数职业程序员并没有保卫 Dijkstra 对于计算机科学的解释的荣耀。我们必须在实验的领域里工作,正如一部分,但非所有的物理学家做的那样。如果三十年后,编程可以在不进行任何实验的前提下进行,这将是计算机科学的一个巨大成就。

你需要进行的实验包括:

用小的例子测试系统以验证它们遵循文档,或者在没有文档时,理解它们的反应;
测试一些小的代码修改去验证它们是否确实修复了一个 bug;
由于对一个系统不完全的理解,需要在两种不同情况下测量它们的性能表现;
检查数据的完整性;
对困难的或者难以重现的 bug,收集解决方案中可能提示的统计数据。
我不认为在这篇文章里我可以讲述实验的设计,你会在实践中学习到这方面的知识。然而,我可以提供两点建议:

第一,对你的假设或者你要测试的断言要非常清楚。把假设写下来也是很有用的,尤其是如果你有点迷惑或者与其他人合作时。

第二,你会经常发现你必须设计一系列的实验,它们中的每个都基于对最后一个实验的理解。所以,你应该设计你的实验尽量去提供最多的信息。但不幸的是,这会让实验保持简单变的困难 - 你必须通过经验来提升这种权衡的能力。

如何发现信息

你所搜寻的事情的本质决定了你应该如何去寻找它。

如果你需要客观的而且容易辨认的关于具体事物的信息,例如一个软件的最新补丁版本,可以在 Internet 搜索,礼貌的询问很多的人,或者发起一个讨论组。不要在网上搜索任何带有观点或主观解释的东西:能够抵达真相的概率太低了。

如果你需要“一些主观的普遍知识”,人们对这些东西已有的思考历史,那就去图书馆吧。例如,想要了解数学,蘑菇或着神秘主义,就去图书馆吧。

如果你需要知道如何做一些琐碎的事情,找两三本关于这个主题的书,仔细阅读。你可以从网络上学到如何做好这些琐碎的事情,比如安装一个软件包。你甚至可以学到一些重要的东西,例如好的编程技术,但相比读一本纸质书的相关部分,你很容易花更多时间在搜索和对结果排序,以及评估结果的权威性。

如果你需要可能没有人知道的信息,例如,“这个新品牌的软件在海量数据的情况下能工作吗”,你仍然必须在网络和图书馆里搜索。在这些选项都完全竭尽后,你可能需要设计一个实验来搞清楚这个问题。

如果你需要一些考虑了某些特殊环境的观点或估值,和一个专家聊聊。例如,如果你想要知道用 Lisp 构建一个现代数据库管理系统是否是一个好主意,你应该和一个 Lisp 专家和一个数据库专家聊一聊。

如果你想要知道它具体是怎样的,比如一个还未发布的在一个特定程序上更快的算法,跟一些在这个领域工作的人聊聊。

如果你想要做一个只有你自己能做的个人决定,比如你是否应该开始某个事业,尝试把一些对这个想法有益和有害的点列出来。如果这没有什么用,做一些预测。假设你已经从各个角度研究了这个想法,并且做了所有该做的准备,在心里列举所有的后果,包括好的和坏的,但你仍可能犹豫不决。你现在应该遵循你自己内心的想法,然后让你的大脑停止思考。大多数可用的预测技术都对决定你内心一半的欲望有作用,因为它们在体现你自己完全多义和随机模式的潜意识都很有用。

如何睿智地写文档

人生太短,不能写没人会读的废话,如果你写了废话,没人会去读。所以好一点的文档是最好的。经理不会去理解这些东西,因为不好的文档会给他们错误的安全感以至于他们不敢依赖他们的程序员。如果一些人绝对坚持你真的在写没用的文档,就告诉他们“是的”,然后安静的找一份更好的工作。

没有其他事情比精确估计 把好的文档转为放松文档要求的估计 更为有效率。真相是冷酷而艰难的:文档,就像测试,会花比开发代码多几倍的时间。

首先,写好的文档是好的写作。我建议你找一些关于写作的事情,学习,练习他们。但即使你是一个糟糕的写手或者对你需要写文档的语言掌握不好,这条黄金规则是你真正需要的:己所不欲,勿施于人。花时间去确实地思考谁会读你的文档,他们从文档中想要获得的真正的东西是什么,并且你可以如何把这些东西交给他们。如果你这样做,你将会变成一个超过平均水平的文档编写者,和一个好的程序员。

当代码可以自成文档时,与提供文档给非程序员看相反,我认识的最好的程序员们有这样一个普遍的观点:编写具有自我解释功能的代码,仅在你不能通过代码清晰解释其含义的地方,才写注释。有两个好的原因:第一,任何人需要查看代码级别的文档大多数情况下都能够并且更喜欢阅读代码。不可否认的,有经验的程序员似乎比初学者更容易做到这件事,然而,更重要的是,没有文档的话,代码和文档不会是自相矛盾的。源代码最糟糕的情况下可能是错误并且令人困惑的。没有完美编写的文档,可能说谎,这可糟糕一千倍。

负责任的程序员也不能让这件事变得更简单些。如何写自解释的代码?那意味着什么?它意味着:

编写知道别人会去阅读的代码(译者注:编写给人看的代码)
运用黄金法则
选择直接的解决方案,即使你可以更快地获得另一个解决方案
牺牲那些可能混淆代码的小的优化
为读者考虑,把你珍贵的时间花在让她更加容易阅读的事情上,并且
永远不要使用这样的函数名比如 foo,bar, 或 doIt!

如何使用源代码控制

源代码控制系统(又称版本控制系统)让你高效地管理工程。他们对一个人是很有用的,对一个团队是至关重要的。它们追踪不同版本里的所有改变,以至于所有代码都未曾丢失,其含义可以归属于改变。有了源代码控制系统,一个人可以自信地写一些而半途而废的代码和调试的代码,因为你修改的代码被仔细地与提交的、官方的即将与团队共享或发布的代码分割开。

我挺晚才开始意识到源代码控制系统的好处,但现在即使是一个人的工程,我也不能离开源代码控制系统。当你们团队在同样的代码基础上工作时,通常它们是必要的。然而,它们有另一个巨大的优点:它们鼓励我们把代码当做一个成长的有机系统。因为每个改变都会被标记为带有名字或数字的修正,一个人会开始认为软件是一种可见的一系列渐进的提升。我认为这对初学者是尤其有用的。

使用源代码控制系统的一个好的技术是一直保持在几天后提交更新。在提交后,一定程度上不活跃,不被调用的代码在几天内都不会完成,因此也不会对其他任何人产生任何问题。因提交错误的代码而降低你队友的开发速度是一个严重的错误,这往往是一种禁忌。

如何进行单元测试

单元测试,对独立的代码功能片段,由编写代码的团队进行测试,也是一种编码,而非与之不同的一些事情。设计代码的一部分就是设计它该如何被测试。你应该写一个测试计划,即使它只是一句话。有时候测试很简单:“这个按钮看起来好吗?”,有时候它很复杂:“这个匹配算法可以精确地返回正确的匹配结果?”。

无论任何可能的时候,使用断言检查以及测试驱动。这不仅能尽早发现 bug,而且在之后也很有用,让你在其他方面担心的谜题得到解决。

极限编程开发者广泛高效地编写单元测试,除了推荐他们的作品,我不能做更好的事情了。

如何与不好相处的人相处

你可能必须和不好相处的人相处。甚至可能你本身就是一个不好相处的人。如果你是那种与同事和权威人物有许多矛盾的人,你应该珍惜这种独立所暗示的东西,但需要在不牺牲你的智力或原则的前提下提高你的人际交往能力。

在这方面没有什么经验,或者先前生活的行为模式在工作场合的经验不能适用的一些程序员,对这种事情会非常困扰。不好相处的人经常习惯于拒绝,并且与他人相比,他们更不容易受社交压力所影响。关键是合适地尊重他们,而非你可能想做的事,但不要充分地满足他们想要的(译者注:他们想要的往往是过分的)。

程序员必须作为一个团队一起工作。当分歧出现时,它必须用某种方式解决,它不能被长时间挂起。不好相处的人通常是极度聪明的,并且有一些很有用的意见可以发表。不带对这个人的偏见,倾听并理解不好相处的人是至关重要的。失败的交流通常是分歧的基础,但它有时候可以被巨大的耐心移除。尝试冷静诚恳地保持交流,并且不接受任何可能产生更大矛盾的引诱。在一个合理的尝试理解的周期后,再做决定。

不要让一个恶霸强迫你做你所不同意的事情。如果你是老大,做你认为最好的事情。不要为任何个人因素做出决定,并时刻准备好为你的决定做出解释。如果你是一个有着不好相处的同事的团队成员,不要让老大的决定有任何个人影响。如果没有按你的想法发展,全身心地按(已成事实的)另一种方法去做。

不好相处的人能够改变与进步。我曾亲眼目睹这种情况,但这很稀少。然而,每个人都有暂时的高兴与失落情绪。

每个程序员但尤其是领导都会面临这样一个挑战:让不好相处的人保持完全的忙碌。他们比别人更倾向于枯燥的工作,并且更能被动地忍受。

如何在时间与空间权衡

没有上过大学的话,你也可以成为一个好的程序员,但你不知道基本的计算复杂度理论的话,你不可能成为一个好的进阶程序员。你不需要知道‘O’的定义,但我个人认为你应该理解‘常量时间’,‘nlogn’,’n²’的区别。你可能可以不靠这方面的知识,凭直觉知道如何在时间和空间之间权衡,但没有这种知识,你将不会有一个和你同事交流的稳固基础。

在设计或理解算法的过程中,算法花费的时间有时候是一个以输入量为自变量的函数。当这种情况发生时,如果运行时间与输入量的对数的 n 倍成正比,我们可以说一个算法的最坏/期望/最好情况运行时间是’nlogn’,这个定义和阐述的方式也可以被应用在数据结构占用的空间上。

对我来时候,计算复杂度理论是美妙的,并且与物理学一样意义深远,并且可能还有很长的路要走!

时间(处理器周期)和空间(内存)可以相互交易。工程是关于妥协的,这就是一个好的例子。它并不总是有条理的,然而,编码一些东西时更加紧凑可以节省空间,但要以解码时花费更多的处理时间为代价。你可以通过缓存节省时间,也就是,花费空间去存储某些东西的一个本地副本,但要以维持缓存的一致性为代价。你偶尔可以通过把更多信息放在一个数据结构里来节省时间。这通常只会有较小的空间占用,但可能会使算法复杂化。

提高时间空间转换经常把它们中的一个或另一个戏剧性地改变。然而,在你开始做这个工作前,你应该问你自己,你将要优化的是否是最需要优化的?研究算法是有趣的,但你不能让这遮蔽了你的双眼让你看不到这样一个冷酷的事实:优化一些不是问题的问题将不会带来任何明显的区别,但却会造成测试的负担。

现代计算机内存越来越便宜,因为不像处理器时间,你在达到边界前你不能看见它,但这种失败是灾难性的。使用内存也有隐藏的代价,比如你影响了其他需要被保留的程序,以及你分配和释放内存的时间。在你想要花更多空间去换取速度之前,请仔细考虑这一点。

当 Michael Tiemann 在 MCC 的时候,人们会站在他的门外面倾听他击键的声音,这种声音是如此的急促以至于难以分辨。

如何分析数据

当你检查一个商业活动并且发现了把它转换为软件应用程序的需求时,数据分析是软件开发早期的一个过程。这是一个官方的定义,当你,一个程序员,应该集中注意力在写别人设计的东西的代码时,这可能会让你相信数据分析是一种更应该归入系统分析的行为。如果我们严格遵循软件工程范式,这可能是正确的。有经验的程序员会成为设计者,最尖锐的设计者变成商业分析师,因此被冠名去思考所有数据需要,并且给你充分定义的任务去执行。这不完全是对的,因为数据是每种编程活动的核心。不管你在你的程序里做什么,你不是在移动数据就是在修改数据。商业分析师分析的是更大尺度上的需要,软件设计者更加压榨这个比例以至于,当问题在你的桌上落地时,好像你需要做的所有事情是应用聪明的算法,开始移动已经存在的数据。

不是这样的。

不管你开始观察它的是哪个阶段,数据是一个良好设计的应用程序主要考虑的因素,如果你仔细观察一个数据分析师是怎么从客户请求中获取需求的,你会意识到,数据扮演了一个基本的角色。分析师创建了所谓的数据流表,所有的数据源被标记出来,信息的流动被塑造出来。清晰定义了什么数据应该是系统的一部分,设计师将会用数据关系,数据交换协议,文件格式的形式塑造数据源,这样任务就准备好传递给程序员了。然而,这个过程还没结束,因为你(程序员)在这个周密的数据提取过程后,需要分析数据以用最好的可能方式表现任务。你的任务的底线是 Niklaus Wirth,多种语言之父,的金句:“算法+数据结构=程序”。这永远不是一个独立的自嗨的算法。每个算法都至少被设计去做一些至少与一段数据相关的事情。

因此,由于算法不会在真空中滚动轮子,你需要分析其他人已经为你标记好的数据和必须写入代码的必要的数据。 一个小例子会使得事情更清楚。实现一个图书馆的搜索程序时,通过你的说明书,用户用类型/作者标题/出版社/出版年份/页数来选择书本。你的程序的中级目标是提供一个合法的 SQL 语句去搜索后端数据库。基于这些需要,你有几个选择:按顺序检查每个控制条件,使用一个 switch 语句,或者几个 if 语句;用一个数据控制数组,把它们与一个事件驱动引擎相连。

如果你的需求也包括提高查询性能,通过确认每个项在一个特殊顺序里,你可能考虑使用组件树去构建你的 SQL 语句。正如你可以看到的,算法的选择依赖于你决定使用或将要创建的数据。这样的决定产生高效算法和糟糕算法间的区别。 然而,效率不是唯一要考虑的因素。你可能在你的代码里使用一打命名变量,让它变得尽可能高效。但这样一段代码可能不能容易地维护。可能为你的变量选择一种合适的容器可以保持相同的速度,此外,在的你同事明年看代码的时候,让他们能够更好地理解代码。更多的,选择一个良好设计的数据结构可能允许他们在不重写代码的前提下,拓展你的代码的功能。长久看来,你对数据的选择决定了你结束代码的工作后,它能工作多久。

让我给你看另一个例子,只是一些思想粮食,让我们假设你的任务是找到字典里超过三位的同字异构词(一个异构词必须在同样的字典里有另一个词)。如果你把这当做一个计算任务,你将会结束于无尽的,尝试找出每个单词的所有组合,然后拿它跟列表里的所有其他单词比较,这样一个无尽的努力中。然而,如果你分析了手头的数据,你会意识到,每个单词可能被一个包含这个词本身以及用它的字母作为 ID 的排序数组的记录所代表,这个蛮力算法可能需要运行几天,而小的那个算法只是一件几秒的事。下次面对一个棘手的问题时,记住这个例子。