传统智慧认为,项目一旦进入编码阶段,工作主要就是机械地把设计转换成可执行语句。这种态度是许多程序丑陋、低效、结构糟糕、不可维护和完全错误的最大一个原因。

31、靠巧合编程

作为开发者,我们也工作在雷区,每天都有从百上千的陷阱在等着抓住我们。

怎样靠巧合编程

有时候我们可能会依靠巧合编程,代码好像能工作,但那不过是一种巧合。

  • 实现的偶然

    • 它也许不是真的能工作,只是看起来能工作
    • 你依靠的边界条件也许只是一个偶然
    • 没有计入文档的行为可能会随着库的下一次发布而变化
    • 多余的和不必要的调用会使你的代码变慢
    • 多余的调用还会增加引入它们自己的新bug的风险
  • 语境的偶然

    模块必须依靠给你的GUI,系统依靠说英语的用户、有文化的用户、没有保证的东西

  • 隐含的假定

    在所有层面上,人们都在头脑里呆着许多假定工作,但这些假定很少被计入文档,而且在不同的开发者之间常常是冲突的。

不要靠巧合编程

怎样深思熟虑编程

  • 总是意识到你在做什么吗
  • 不要盲目地编程
  • 按照计划行事,不管计划是在你的头脑中,在鸡尾酒餐巾的背面,还是在某个case工具生成的墙那么大的输出结果上
  • 依靠可靠的事物
  • 为你的假定建立文档
  • 不要只是测试你的代码,还要测试你的假定
  • 为你的假定建立文档
  • 不要只是测试你的代码
  • 为你的工作划分优先级
  • 不要做历史的奴隶

32、算法速率

估计算法使用的资源:时间、处理器、内存等常常至关重要。

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

大多数并非微不足道的算法都要处理某种可变的输入:排序n个字符串、对m*n矩阵求逆、用n位的密钥解密消息等,这些输入的规模会影响算法,输入越多,运行时间久越长,或者使用内存越多。

O()表示法

O()表示法是处理近似计算的一种数学途径。

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

O(1)		常量型(访问数组元素、简单语句)
O(lg(n))	对数型(二分查找)[lg(n)是log2(n)的简写]
O(n)		线性型(顺序查找)
O(nlg(n))	比线性差,但不会差很多(快速排序、堆排序的平均运行时间)
O(n²)		平方律型(选择和插入排序)
O(n³)		立方型(2n × n矩阵相乘)
O(C^n)		指数型(旅行商问题,集合划分)

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

常识估算

  • 简单循环

    简单循环从1运行到n,那么算法很可能是O(n):时间随n线性增加。如:穷举查找、找到数组中的最大值、生成校验和

  • 嵌套循环

    循环嵌套循环复杂度为O(m × n)。如:冒泡排序算法往往是O(n²)

  • 二分法

    每次循环是讲事物一分为二则很可能是对属性算法O(lg(n))。如:二分查找、遍历二叉树、查找机器字中第一个位置的位。

  • 分而治之

    划分输入并独立地在两个部分上进行处理,然后组合结果。快速排序尽管在技术上是O(n²),但其行为在输入的是排过序的输入时会退化,平均运行时间是O(nln(n)).

  • 组合

    只要算法考虑事物的排列,其运行时间就有可能失去控制,这是因为排列涉及到阶乘。特定问题领域中,常常用启发式方法减少这些类型的算法的运行时间。

实践中的算法速率

估算你的算法的阶

在你的职业生涯中,不大可能花费大量时间编写排序裂成,如果不付出相当努力,现有库中的例程可能会胜过你编写的任何东西。但基本的算法类型会不时地的再度出现,考虑一下大量数据对运行时间会内存的消耗可能带来的影响。

测试你的估算

既考虑理论问题,又考虑实践问题,在进行所有这些估算后,唯一作数的计时是你的代码运行在实际工作环境中,处理真实数据的速率。

最好的并非总是最好的

你还需要在选择适当算法时注重实效。最快的算法对于你的工作并非总是最好的。对于小输入级,如果你的算法有高昂的设置开销,设置时间可能会使得运行时间相形见绌,并使得算法变得不再适用。

33、重构

随着程序的烟花,我们有必要重新思考早先的决策,并重写代码。代码不是静态事物,需要演化。软件的工作方式与建筑并不怎么相似,软件更像园艺。

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

应在何时重构

当你遇到绊脚石,代码不在合适,有两样东西其实应该合并或是其他任何对你来说是错误的东西时,应该现在就做。

应该考虑重构代码的特征:

  • 重复
  • 非正交的设计
  • 过时的知识
  • 性能

重构代码可能相当痛苦,它几乎已经在工作,显示事实上却要被撕毁,许多开发者不愿意只是因为代码不完全正确就撕毁代码。

时间压力常常被用作不进行重构的借口,但是这个借口并不成立,现在没能重构,岩土修正问题将需要投入更多的时间,考虑更多的依赖关系。

早重构、常重构

追踪需要重构的食物,如果不能立即重构,就一定要把它列入计划。确保受影响的代码的使用者知道该代码计划要重构,以及这可能会怎样影响他们。

怎样进行重构

就其核心而言,重构就是重新设计。你或你的团队的其他人设计的任何东西都可以根据新的事实、更深的理解、变化的需求等等,重新进行设计。

重构是一项需要慎重、深思熟虑、小心进行的活动,进行利大于弊的重构的提示:

  • 不要试图在重构的同时增加功能
  • 在重构之前,确保你拥有良好的测试,尽可能经常运行这些测试
  • 采取短小、深思熟虑的步骤

34、易于测试的代码

我们需要在一开始就把可测试行构建进软件中,并且在把各个部分连接在一起之前对每个部分进行彻底的测试。

单元测试

软件的单元测试是对模块进行演练的代码,在典型的情况下,单元测试将建立某种人工环境,然后调用被测试模块中的例程,根据已知的值,或是同一测试先前返回的结果,对返回的结果进行检查。随后,当我们将模块集成到完整系统时,有信心各个部分都能够如预期那样工作。

针对合约进行测试

我们喜欢把单元测试视为针对合约的测试,编写测试用例,确保给定的单元遵守其合约,这将告诉我们两件事:代码是否符合合约,合约的含义是否与我们所认为的一样。

通过强调针对合约进行测试,我们可以设法尽可能多地避免那些下游的灾难。

为测试而设计

当你设计模块甚至是单个例程时,应该既设计器合约,也设计测试该合约的代码。通过设计能够通过测试、并履行合约的代码,你可以仔细地考虑边界条件和其他非如此便不会发现的问题。

编写单元测试

摸的单元测试部应该被扔在源码树的某个遥远的角落,它们必须放置在方便的地方。如果你不容易找到它,也就不会使用它。

易于找到的测试代码能给使用你代码的开发者提供两样无价的资源:

  • 一些说明怎样使用你的模块所有功能的例子
  • 用以构建回归测试、以验证未来对代码的任何改动是否正确的手段

只提供单元测试还不够,你还必须运行它们,并且经常运行它们。

使用测试装备

我们通常会编写大量测试代码,并进行大量测试,为项目开发标准的测试装备,可以让自己生活的更容易点。

测试装备可以处理一些常用操作,比如记录状态、分析输出是否符合预期结果,以及选择和运行测试。

不管你决定采用的技术是什么,测试装备都应该具有以下功能:

  • 用以指定设置与清理的标准途径
  • 用以选择个别或所有可用测试的方法
  • 分析输出是否是预期(或意外)结果的手段
  • 标准化的故障报告形式

构建测试窗口

即使是最好的测试,也不可能找出所有的bug,一旦某个软件部署之后,在现实世界的数据正流过他的血脉时,你常常需要对其进行测试。

含有跟踪消息的日志文件可用提供内部状态的各种视图,而又不使用调试器。日志消息的格式应该规范、一致,以便阅读和自动解析。

测试文化

你编写的所有软件都将进行测试,如果不是你和你们团地测试,那就要由最终用户测试。预先的测试计划可用大大降低维护费用、减少客户服务电话。

测试是技术,但更是文化。

测试你的软件,否则你的用户就得测试

35、邪恶的向导

应用自身始终在变的更复杂。现在的大多数开发都使用多层模型,可能还伴有某种中间件层或事务监控器。

工具制造商和基础设施提供商带着魔弹“向导”来了,你可用用向导创建服务器组件,实现Java beans、处理网络接口等所有复杂的、最好有专家帮助的领域。

如果你真的使用向导,却步理解他制作出来的所有代码,你就无法控制你自己的应用。你没有能力维护他,而且在调试时会遇到很大的困难。

不要使用你不理解的向导代码

向导的代码变成了我们应用的完整组成部分,与我们编写的功能交织在一起,不再是向导的代码,没有人应该制作他们不完全理解的代码。

更多有关《程序员修炼之道》的读书笔记,请关注 :
http://tabalt.net/blog/the-pragmatic-programmer-reading-notes/

本文链接:http://tabalt.net/blog/tpp-while-you-are-coding/,转载请注明。