程序员修炼之道笔记

1. 不要容忍破窗户(Don’t Live with Broken Windows)

破窗户
一扇破窗户,只要有那么一段时间不修理,就会渐渐给建筑的居民带来一种废弃感,于是又一扇窗户破了,人们开始乱扔垃圾,出现了乱涂乱画,严重的结构损坏开始了,在相对较短的一段时间里,建筑就被损毁得超出了业主愿意修理的程度,而废弃感成了现实。

不要留着“破窗户”(低劣的设计,错误决策,或是糟糕的代码)不修。发现一个就修一个,如果没有足够的时间进行适当的修理,就用木板把它打起来,或许你可以把出问题的代码放入注释,或是用虚设的数据加以替代,采取某种行动防止进一步的损坏,并说明情势处在你的控制之下。

2. 定期为你的知识资产投资(Invest Regularly in Your Knowledge Portfolio)

我们喜欢把程序员所指导的关于计算技术和他们所工作的应用领域的全部事实、以及他们的所有经验视为他们的知识资产,管理知识资产与管理金融资产非常相似。

  • 定期投资。就像金融投资一样,你必须定期为你的知识资产投资,即使投资量很小,习惯自身也和总量一样重要。
  • 多元化。你知道的不同的事情月多,你就越有价值。作为底线,你需要知道你目前所用的特定技术的各种特性。但不要就此止步。计算技术的面貌变化很快----今天的热门技术明天就可能变得近乎无用,或至少不再抢手,你掌握的技术越多,你就越能更好地进行调整,赶上变化。
  • 管理风险。从高风险、可能有高回报,到低风险、低回报,技术存在于这样一条谱带上,把你所有的金钱都投入可能突然崩盘的高风险股票并不是一个好主意;你也不应太保守,错过可能的机会。不要把你所有的技术鸡蛋放在一个篮子里。
  • 低买高卖。在新兴的技术流行之前学习它可能就和找到被低估的股票一样困难,但所得到的就和那样的股票带来的收益一样。
  • 重新评估和平衡。这是一个非常动荡的行业,你上个月开始研究的热门技术现在也许已像石头一样冰冷。也许你需要重温你有一阵子没有使用的数据库技术,有或许,如果你之前试用过另一种语言,你就会更有可能获得那个新职位。

3. 不要重复劳动(Don’t Repeat Yourself)

重复劳动是怎样发生的?

  • 强加的重复劳动。开发者觉得他们无可选择----环境似乎要求重复劳动。
  • 无意的重复劳动。开发者没有意识到他们在重复信息。
  • 无耐性的重复劳动。开发者偷懒,他们重复劳动,因为那样似乎更容易。
  • 开发者之间的重复劳动。同一团队或不同团队的几个人重复了同样的信息。

你所需要做的是营造一种环境,在其中找到并复用已有的东西,比自己编写更容易。如果不容易,大家就不会去复用。而如果不进行复用,就会有重复劳动的风险。

4. 消除无关事物之间的影响(Eliminate Effects Between Unrelated Things)

如果你想要制作易于设计、构建、测试及扩展的系统,正交性是一个十分关键的概念。一旦学会了直接应用正交性原则,你将会发现,你制作的系统的质量立刻就得到了提高。“正交性”是从几何学中借来的术语,如果两条直线相交成直角,它们就是正交的。在计算技术中,该术语用于表示某种不相依赖性或是解耦性。如果两个或更多事物中的一个发生变化,不会影响其他事物,这些事物就是正交的。在设计良好的系统中,数据库代码与用户界面是正交的,你可以改动界面,而不影响数据库,更换数据库,而不用改动界面。

我们想要设计自足的组件:独立,具有单一、良好定义的目的。如果组件是相互隔离的,你就知道你能够改变其中之一,而不用担心其余组件。只要不改变组件的外部接口,就不会造成波及整个系统的问题。编写正交的系统有两个好处:提高生产效率与降低风险。

  • 提高生产效率
    • 改动得以局部化,所以开发时间和测试时间得以降低。与编写单个的大块代码相比,编写多个相对较小的、自足的组件更为容易。可以设计、编写简单的组件,对其进行单元测试,然后把它们忘掉-----当增加新代码时,无须不断改动已有的代码。
    • 正交的途径还能够促进复用。如果组件具有明确而具体的、良好定义的责任,就可以用其最初的实现者未曾想象过的方式,把它们与新组件组合在一起。
    • 如果你对正交的组件进行组合,生产效率会有相当微妙的提高。假定某个组件做M件事情,而另一个组件做N件事情,如果它们是正交的,而你把它们组合在一起,结果就能做的事情就更少。
  • 降低风险
    • 有问题的代码区域被隔离开来。如果某个模块有毛病,它不大可能把病症扩散到系统的其余部分,要把它切掉,换成健康的新模块也更容易。
    • 所得系统更健壮。对特定区域做出小的改动与修正,你所导致的任何问题都将局限在该区域中。
    • 正交系统很可能得到更好的测试。因为设计测试、并针对其组件运行测试更容易。
    • 不会与特定的供应商、产品、或是平台绑在一起,因为这些第三方组件的接口将被隔离在全部开发的较小部分中。

5. 要修正问题,而不是发出指责(Fix the Problem,Not the Blame)

bug是你的过错还是别人的过错,并不是真的很有关系,它仍然是你的问题

对于许多开发者、调试本身是一个敏感、感性的话题。你可能会遇到抵赖、推诿、蹩脚的接口、甚或是无动于衷,而不是把它当作要解决的难题发起进攻。
要接受事实:调试就是解决问题,要据此发起进攻。
发现了他人的bug之后,你可以花费时间和精力去指责让人厌恶的肇事者,在有些工作环境中,这是文化的一部分,并且可能是“疏通剂”。但是,在技术竞技场上,你应该专注于修正问题,而不是发出指责。

6. 编写能编写代码的代码(Write Code That Write Code)

当木匠面临一再地重复制作同一样东西的任务时,他们会取巧。他们给自己建造夹具或模板,一旦他们做好了夹具,他们就可以反复制作某样工件,夹具带走了复杂性,降低了出错的机会,从而让木匠能够自由地专注于质量问题。
作为程序员,我们常常发现自己也处在同样的位置上。我们需要获得同一种功能,但却是在不同的语境中。我们需要在不同的地方重复信息。有时我们只是需要通过减少重复的打字,使自己免于患上腕部劳损综合症。
以与木匠在夹具上投入时间相同的方式,程序员可以构建代码生成器。一旦构建好,在整个项目生命期内都可以使用它,实际上没有任何代价。

代码生成器有两种主要类型:

  • 被动代码生成器只运行一次来生成结果,然后结果就变成了独立的----它与代码生成器分离了。
  • 主动代码生成器在每次需要其结果时被使用。结果是用过就扔的----它总是能由代码生成器重新生成。主动代码生成器为了生成其结果,常常要读取某种形式的脚本或控制文件。

代码生成器不一定要很复杂,最复杂的部分通常是负责分析输入文件的解析器。让输入格式保持简单,代码生成器就会变得简单。
代码生成器不一定要生成代码,可以用它生成几乎任何输出:HTML、XML----可能成为项目中别处输入的任何文本。

7. 你不可能写出完美的软件(You Can’t Write Perfect Software)

在计算技术简短的历史中,没有一个人曾经写出过一个完美的软件。你也不大可能成为第一个。

我们不断地与他人的代码结合----可能不符合我们的高标准的代码----并处理可能有效,也可能无效的输入,所以我们被教导说,要防卫性地编码。如果有任何疑问,我们就会验证给予我们的所有信息,我们使用断言检测坏数据。我们检查一致性,在数据库的列上施加约束,而且通常对自己感到相当满意。但注重实效的程序亚un会更进一步,他们连自己也不信任,知道没有人能编写完美的代码,包括自己,所以注重实效的程序员针对自己的错误进行防卫性的编码。

8. 如果它不可能发生,用断言确保它不会发生(If It Can’t Happen,Use Assertions to Ensure That It Won’t)

“这些代码不会被用上30年,所以用两位数字表示日期没问题。”“这个应用决不会在国外使用,那么为司马要使其国际化?”“count不可能为负”“这个printf不可能失败”

无论何时发现自己在思考“但那当然不可能发生”,增加代码检查它,最容易的方法是使用断言。当然,传给断言的条件不应该有副作用,还要记住断言可能会在编译时被关闭----决不要把必须执行的代码放在assert中。
不要用断言代替真正的错误处理,断言检查的是绝不应该发生的事情。

9. 将抽象放进代码,细节放进元数据(Put Abstraction in Code,Details in Metadata)

细节会弄乱我们整洁的代码----特别是如果它们经常变化。每当我们必须去改动代码,以适应商业逻辑、法律或管理人员个人一时的口味的某种变化时,我们都有破坏系统或引入新bug的危险。所以说“把细节赶出去”,把它们赶出代码,当我们在与它作斗争时,我们可以让我们的代码变得高度可配置和“软和”,也就是,容易适应变化。
元数据是什么?严格地说,元数据是关于数据的数据。最常见的例子可能是数据库schema或数据词典。schema含有按照名称、存储长度及其他属性,对字段进行描述的数据。

我们的目标是以声明方式思考(规定要做什么,而不是怎么做),并创建高度灵活和可适应的程序。我们想要尽可能多地通过元数据配置和驱动应用。我们的目标是以声明方式思考(规定要做什么,而不是怎么做),并创建高度灵活和可适应的程序。我恶魔年通过采用一条一般准则来做到这一点:为一般情况编写程序,把具体情况放在编译的代码库之外。这种方法有若干好处:

  • 它迫使你解除设计的耦合,从而带来更灵活、可适应性更好的程序。
  • 它迫使你通过推迟细节处理,创建更健壮、更抽象的设计----完全推迟到程序之外。
  • 无需重新编译应用,就可以对其进行定制,还可以利用这一层面的定制,轻松地绕开正在运行的产品系统中的重大bug。
  • 与通用的编程语言的情况相比,可以通过一种大为接近问题领域的方式表示数据。
  • 甚至还可以用相同的应用引擎,但是用不同的元数据,实现若干不同的项目。

10. 用黑板协调工作流(Use Blackboards to Coordinate Workflow)

假定警探已开始在会议室放置了一个大黑板,为侦破案件在上面写下线索,随着资料的累积,某位侦探可能会注意到某种关联,并张贴他的看法和推断,这个过程持续进行,直到案件了结。
黑板方法的一些关键特性:

  • 没有侦探需要知道其他任何侦探的存在,他们查看黑板,从中了解新的信息,并且加上他们的发现。
  • 侦探可能接受过不同的训练,具有不同程度的教育背景和专业经验,甚至有可能是在同一管辖区工作。他们都渴望破案,但这就是全部共同点。
  • 在这个过程中,不同的侦探可能会来来去去,并且工作班次也可能不同。
  • 对放在黑板上的内容没有什么限制,可以是图片、判断、物证等等。

我们可以用黑板协调完全不同的事实和因素,同时又使参与方保持独立,甚至隔离。
当然,可以用更蛮力的方法获得相同的结果,但得到的将是更脆弱的系统,当它出故障时,出动所有人马也许都无法使你的程序再工作起来。
也可以使用工作流系统,设法处理每一种可能的组合和情况,存在许多这样的系统,但它们可能会很复杂,并且需要许多程序员。当规章制度发生变化时,工作流也必须重新组织:人们也许必须改变他们的流程,硬性连接的代码也许必须重写。
数据到达的次序无关紧要,在收到某项事实时,它可以触发适当的规则,反馈也容易处理,任何规则集的输出都可以张贴到黑板上,并触发更为适用的规则。

11. 估算算法的阶(Estimate the Order of Your Algorithms)

O()表示法时处理近似计算的一种数学途径,当我们写下某个特定的排序例程对n个记录进行排序所需的时间是O(n2)时,我们的意思是,在最坏的情况下,所需时间随n的平方变化,使记录数加倍,时间大约将增加4倍,把O视为“阶为。。。”的意思。O(n)表示法对我们在度量的事物(时间、内存等等)的值设置了上限,如果我们说某函数需要O(n2)时间,那么我们就知道它所需时间的上限不会比n2增长得更快。有时我们会遇到相当复杂的O()函数,但因为随着n的增加,最高阶的项主宰函数的值,习惯做法是去掉所有低阶项,并且对任何常数系数都不予考虑,O(n2/2+3n)和O(n2/2)一样,后者又与O(n2)等价。这实际上是O()表示法的一个弱点,某个O(n2)算法可能比另一个O(n2)算法要快1000倍,但你从表示法上却看不出来。

可以使用常识估算许多基本算法的阶

  • 简单循环: 如果某个简单循环从1运行到n,那么算法很可能就是O(n),时间随n线性增加。其例子有穷举查找、找到数组中的最大值、以及生成校验和。
  • 嵌套循环: 如果你需要在循环中嵌套另外的循环,那么算法法就变成了O(m*n),这里的m和n是两个循环的界限。这通常发生在简单的排序算法中,比如冒泡排序:外循环依次扫描数组中的每个元素,内循环确定在排序结果的何处放置该元素,这样的排序算法往往是O(n2)。
  • 二分法: 如果算法在每次循环时把事物集合一分为二,那么它很可能是对数型O(lg(n))算法。对有序列表的二分查找、遍历二叉树,都可能是O(lg(n))算法。
  • 分而治之: 划分其输入,并独立地在两个部分上进行处理,然后再把结果组合起来的算法可能是O(nln(n))。经典例子是快速排序、其工作方式是:把数据划分为两半,并递归地对每一半进行排序。尽管在技术上是O(n2),但因为其行为在馈入的是排过序的输入时会退化,快速排序的平均运行时间是O(nln(n))。
  • 组合: 只要算法考虑事物的排序,其运行时间就有可能失去控制,这是因为排序涉及阶乘。得出5个元素的组合算法所需的时间:从数字1到5有5!=54321=120中排列,6个元素需要6倍的时间,7个元素则需要42倍的时间。其例子包括许多公认的难题算法:旅行商问题,把东西最优地包装进容器中,划分一组数,使每一组都有相同的总和,等等。在特定问题领域中,常常用启发式方法减少这些类型的算法的运行时间。

最好的并非总是最好的

假定输入集很小,直截了当的插入排序的性能将和快速排序一样好,而你用于编码和调试的时间将更少。如果你的选择的算法有高昂的设置开销,你也需要注意,对于小输入集,这些设置时间可能会使运行时间相形见绌,并使得算法变得不再适用。

12. 要通过全部测试,编码才算完成(Coding Ain’t Done Til All the Tests Run)

大多数开发者都讨厌测试。许多团队都会为他们的项目精心制定测试计划。我们发现,使用自动测试的团队成功的机会要大的多。与待在架子上的测试计划相比,随每次构建运行的测试要有效得多。
事实上,好的项目拥有的测试代码可能比产品代码还要多,编写这些测试代码所花的时间是值得的。长远来看,它最后会便宜得多,而实际上有希望制作出接近零缺陷的产品。此外,知道你通过了测试将给你高度的自信:一段代码已经“完成”了。

  • 测试什么

    • 单元测试
      单元测试是对某个模块进行演练的代码,如果各组成部分自身不能工作,它们结合在一起多半也不能工作,使用的所有模块都必须通过它们自己的单元测试,然后才能继续前进。
    • 集成测试
      集成测试说明组成项目的主要子系统能工作,并且能很好的协同。如果在适当的地方有和好的合约,并且进行了良好的测试,我们就可以轻松地检测到任何集成问题,否则集成就会变成肥沃的bug繁殖地。事实上,它常常是系统的bug来源最大的一个。
    • 验证和校验
      一旦有了可执行的用户界面或原型,你需要回答一个最重要的问题,用户告诉了你他们需要什么,但那是他们需要的吗?它满足系统的功能需求吗?这也需要进行测试。要注意用户的访问模式,以及这些模式与开发者所用的测试数据的不同。
    • 资源耗尽、错误及恢复
      现在已经很清楚,系统在理想条件下将会正确运行,你需要知道的是,它在现实世界的条件下将如何运行,在现实世界中,程序没有无限的资源,它们会把资源耗尽。
    • 性能测试
      问问自己,软件是否满足现实世界的条件下的性能需求,预期的用户数、连接数、或每秒事务数,可以伸缩吗?对于有些应用,可能需要专门的测试硬件或软件模拟现实情况下的负载。
    • 可用性测试
      可用性测试与到目前为止讨论过的其他测试类型不同,它是由真正的用户、在真实环境条件下进行的。根据人的因素考察可用性,需要处理需求分析过程中的任何误解吗?软件对于用户,就像手的延伸吗?
      没能满足可用性标准就像是除零错误,是个大bug。
  • 怎样测试

    • 回归测试
      回归测试把当前测试的输出与先前的或已知的值进行对比,可以确定我们今天对bug的修正没有破坏昨天可以工作的代码。这是一个重要的安全网,它能减少令人不快的意外。到目前为止提到过的所有测试都可作为回归测试运行,确保我们在开发新代码时灭有损失任何领地。我们可以运行回归测试,对性能、合约、有效性等进行校验。
    • 测试数据
      从何处获取运行所有这些数据所需的数据?数据只有两种:现实世界的数据和合成的数据。实际上两者都需要,因为这两类数据的不同性质将揭示出软件中的不同bug。当需要大量数据,可能超过了任何现实世界的样本所能提供的数量,也许可以用现实世界的数据做种子,生成更大的样本集,并且调整特定的有独特需要的字段。当需要能强调边界条件的数据,这些数据可以完全由人工合成。当需要能展现出特定的统计属性的数据,可以给出随即的或有序的数据,以暴露这种弱点。
    • 演练GUI系统
      测试GUI密集型系统常常需要专门的测试工具。这些工具可能基于简单的时间捕捉、重放模型,也可能需要专门编写的脚本驱动GUI,有些系统结合了这两者的基本要素。
    • 对测试进行测试
      因为我们不可能编写出完美的软件,所以我们也不可能编写出完美的测试软件,我们需要对测试进行测试。在你编写了一个测试,用以检测特定的bug时,要故意引发bug,并确定测试会发出提示。这可以确保测试在bug真的出现时抓住它。
    • 彻底测试
      一旦你自信你的测试是正确的,并且正在找出你制造的bug,你怎么知道你已经足够彻底地对代码库进行了测试呢?
      简短的回答是“你不知道”,而且你也不会知道。但市场上有一些产品能够提供帮助。这些覆盖分析工具会在测试过程中监视你的代码,追踪哪些代码行执行过,哪些没有。这些工具能够帮助你从总体上了解测试的全面程度,但别指望看到百分百的覆盖率。
  • 何时测试
    许多项目往往会把测试留到最后一分钟----dead-line马上就要来临时,我们需要比这早的多地开始测试,任何产品代码一旦存在,就需要进行测试。大多数测试都应该自动完成。通常,只需要回归地运行各个单元测试和集成测试,这并不成问题。但有些测试肯呢个不容易这样频繁地运行,例如,压力测试肯呢个需要特殊的设置或设备,以及某种手工操作。这些测试的运行可以不那么频繁,但让它们按照计划定期运行,这很重要,如果无法自动完成,那就确保让它出现在计划中,并给这项人物分配所需资源。

13. 温和地超出用户的期望(Gently Exceed Your Users’ Expectations)

某公司宣布利润创记录,其股价却下跌了20%,当晚的金融新闻解释说,该公司没有实现分析家预期的业绩。一个小孩打开昂贵的圣诞礼物,却大哭起来,这不是他想要的廉价玩具。某个项目团队奇迹般地实现了一个极其复杂的应用,但却遭到用户的抵制,因为该应用没有帮助系统。在现实中,项目的成功是由它在多大程度上满足了用户的期望来衡量的。不符合用户预期的项目注定是失败的,不管交付的产品在绝对的意义上有多好。但是,像希望得到廉价玩具的小孩的父母一样,你走得太远也会失败。

用户在一开始就会带着他们对所需要的东西的想象来到你面前,那可能不完整、不一致、或是在技术上不可能做到,但那时他们的,而且就像过圣诞节的小孩一样,他们也在其中投入了一些感情,你不能简单地忽视它。随着你对他们的需要的理解的发展,你会发现在他们的有些期望无法满足,或是他们的有些期望过于保守。你的部分角色就是要就此进行交流。与你的用户一同工作,以使他们正确地理解你将要交付的产品,并且要在整个开发过程中进行这样的交流。决不要忘了你的应用要解决的商业问题。

如果你和用户紧密协作,分享他们的期望,并同他们交流你正在做的事情,那么当项目交付时,就不会发生多少让人吃惊的事情了。这是一件糟糕的事情,要设法让你的用户惊讶。请注意,不是惊吓他们,而是要让他们高兴。给他们的东西要比他们期望的多一点,给系统增加某种面向用户的特性所需的一点额外努力将一次又一次在商誉上带来回报。只是要记住,不要因为增加新特性而破坏了系统。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值