《程序员修炼之道:从小工到专家》第六章:当你编码时(1)

一、靠巧合编程

怎样靠巧合编程

  • 实现的偶然

实现的偶然是那些只是因为代码在现在的编写方式才得以发生的事情。尽管它们能够工作,但那实在只是一个巧合。

对于你调用的例程,要只依靠记入了文档的行为。如果出于任何原因你无法做到这一点,那就充分把你的各种假设记入文档。

  • 语境的偶然

命令行?GUI?中文?

  • 隐含的假定

巧合可以在所有层面让人误入歧途——从生成需求直到测试。特别是测试,充满了各种虚假因果关系和巧合的输出,很容易假定X是Y的原因,但正如我们在调试中所说的:不要假定,要证明。

在所有层面上人们都在头脑中带着许多假定工作——但这些假定很少被记入文档,而且在不同的开发者之间常常是冲突的。并非以明确的事实为基础的假定都是所有项目的祸害。

不要靠巧合编程。

怎样深思熟虑地编程

我们想要让编写代码所花的时间越少,想要尽可能在开发周期的早期抓住并修正代码错误,想要在一开始就少制造错误。如果我们能够深思熟虑的编程,那这些会对我们有所帮助:

  • 总是意识到你在做什么。
  • 不要盲目的编程。(试图构建你完全不熟悉的应用或者使用你不熟悉的技术)
  • 按照计划行事,不管计划是在你的大脑中还是在别的什么地方。
  • 依靠可靠的事物,不要依靠巧合或假定。
  • 为你的假定建立文档,“按合约设计”。
  • 不要只是测试你的代码,还要测试你的假定。不要猜测,要实际尝试它。编写断言测试你的假定。
  • 为你的工作划分优先级,把时间花在重要的方面。
  • 不要做历史的奴隶。不要让已有的代码支配将来的代码。准备好进行重构

二、算法效率

我们说估算算法是什么意思?

大多数重要的算法都不是线性的,有些时亚线性的,比如二分查找,甚至有些更加复杂,其运行时间或内存需求要远远快于n。

我们发现,只要我们编写的是含有循环或递归调用的程序,我们会下意识检查运行时间和内存需求,这很少是形式过程。有时候我们发现自己在进行更加详细的分析,那就是用上O()表示法的时候了。

O()表示法

O()表示法是处理近似计算的一种数学途径。当我们写下某个特定的排序例程对n个记录进行排序所需的时间是O(n²)时,我们的意思是,在最坏的情况下,所需时间随n的平方变化。

O()表示法对我们在度量的事物的值设置了上限。如果我们说函数需要O(n²)时间,那么我们就知道它所需的时间的上限不会比n²增长的快。

有时我们会遇到相当复杂的O()函数,但因为随着n的增加,最高阶的项将会主宰函数的值,习惯做法是去掉所有低阶项,并且对任何常数系数都不予考虑。这实际上是O()表示法的一个弱点——某个O(n²)算法可能比另一个O(n²)算法要快1000倍,但你从表示法上看不出来。

一些常见的O()表示法:

  • O(1)——常量型(访问数组元素)
  • O(lg(n))——对数型(二分查找)【lg(n)是log₂(n)的简写形式】
  • O(n)——线性型(顺序查找)
  • O(nlg(n))——比线性差,但不会差很多(快速排序、堆排序的平均运行时间)
  • O(n²)——平方律型(选择和插入排序)
  • O(n³)——立方型(2n*n矩阵相乘)
  • O(Cⁿ)——指数型(旅行商问题,集合划分)

O()表示法并非只适用于时间,你可以用它表示算法使用的其他任何资源。

常识估算

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

  • 简单循环:O(n),比如查找数组最大值
  • 嵌套循环:O(m*n)即O(n²),比如冒泡排序
  • 二分法:O(lg(n)),比如二分查找、遍历二叉树
  • 分而治之:O(nlg(n)),划分其输入,并独立地在两个部分上进行处理,然后再把结果组合起来的算法,比如快速排序
  • 组合:O(Cⁿ),只要算法考虑事物的排列,其运行时间就可能失控,这是因为排列涉及到阶乘,比如旅行商问题

实践中的算法效率

估算你的算法的阶

你可以通过一些途径处理潜在问题,如果你有一个O(n²)算法,设法找到能使其降至O(nlg(n))的分而治之的途径。

如果你不能确定代码需要多长时间,或是要使用多少内存,就试着运行它,变化输入记录的数目,然后把结果绘制成图。你应该很快了解到曲线的形状,随着输入量的增加,它是向上弯曲、直线,还是保持平直?

还要考虑你在代码自身中所做的事情,对于较小的n值,简单的O(n²)循环性能可能会比复杂的O(nlg(n))算法更好,特别是O(nlg(n))算法有昂贵的内循环时。

在进行所有这些估算之后,唯一作数的计时是你的代码运行在实际工作环境中、处理真实数据时的速率。

测试你的估算

如果要获得准确的计时很棘手,就用代码剖析器获得你的算法中的不同步骤的执行次数,并针对输入的规模绘制这些数字。

最好的并非总是最好的

三、重构

与修建建筑相比,软件的工作方式更像是园艺。

重写、重做和重新架构代码合起来,称为重构(refactoring)。

你应在何时进行重构

无论代码具有下列哪些特征,你都应该考虑重构代码:

  • 重复。你发现了对DRY原则的违反。
  • 非正交的设计。
  • 过时的知识。
  • 性能。

时间压力常常被用作不进行重构的借口。但现在没能进行重构,沿途修正问题将需要投入多得多的时间——那时将要考虑更多的依赖关系。

早重构,长重构

追踪需要重构的事物,如果你不能立刻重构某样东西,就一定要把它列入计划。

怎样进行重构

就其核心而言,重构就是重新设计。你或你的团队的其他人设计的任何东西都可以根据新的事实、更深的理解、变化的需求、等等,重新进行设计。但如果你毫无节制地撕毁大量代码,你可能会发现自己处在比一开始更糟的位置上。

显然,重构是一项需要慎重、深思熟虑、小心进行的活动:

  • 不要试图在重构的同时增加功能。
  • 在开始重构之前,确保你拥有良好的测试。
  • 采用短小、深思熟虑的步骤,重构往往涉及到进行许多局部改动,继而产生更大规模的改动。如果你使你的步骤保持短小,并且在每个步骤之后进行测试,你将能够避免长时间的调试。

所以,下次你看到不怎么合理的代码时,既要修正它,也要修正依赖于它的每样东西,要管理痛苦:如果它现在有损害,那么以后的损害会更大,你也许最好一劳永逸的修正它。

记住软件的熵中的教训:不要容忍破窗户。

学院君

学院君 has written 548 articles

资深PHP工程师,Laravel学院院长