《程序员修炼之道:从小工到专家》第四章:注重实效的偏执

你不可能写出完美的软件

不完美的系统、荒谬的时间标度、可笑的工具、还有不可能实现的需求——在这样一个世界上,让我们安全“驾驶”。

一、按合约设计

没有什么比常识和坦率更让人感到惊讶。

与计算机打交道很难。与人打交道更难。但作为一个族类,我们花费在弄清楚人们交往的问题上的时间更长。在过去几千年中我们得出的一些解决办法也可应用于编程,确保坦率的最佳方案之一就是合约。

合约既规定你的权利和义务,也规定对方的权利和责任,此外,还有关于任何一方没有遵守合约的后果的约定。

何为DBC

Meyer为Eiffel语言发展了按合约设计的概念。

什么是正确的程序?不多不少,做它声明要做的事情的程序。用文档记载这样的声明,并进行校验,是按合约设计(简称DBC)的核心所在。

软件系统中的每一个函数和方法都会做某件事情。在开始做某件事情之前,例程对世界的状态可能有某种期望,并且有能力称述系统结束时的状态,Meyer这样描述这些期望和称述:

  • 前条件——为调用例程必须为真的条件,例程的需求。违反前条件时,例程不应被调用。传递好数据是调用者的责任。
  • 后条件——例程保证会做的事情,例程完成时世界的状态。不允许有无限循环。
  • 类不变项——从调用者的视角来看,该条件始终为真。在例程内部可能会变,但是例程退出返回调用者时必须为真。

如果任何一方没有履行合约的条款,某种补偿措施就会启用——引发异常或是终止程序。

通过合约进行设计。

实现DBC

使用DBC的最大好处也许是它迫使需求与保证的问题走到前台,输入范围、边界条件、交付什么、不交付什么,不对这些事项作出陈述,就回到了靠巧合编程,那是许多项目开始、结束、失败的地方。

实现方式:

  • 断言,在某些语言中可以通过断言进行模拟,但是断言并不能做DBC做的每一件事情。
  • 语言支持,有内建DBC支持的语言自动在编译器和runtime系统中检查前条件和后条件。

DBC与早崩溃

通过早崩溃,在问题现场找到和诊断问题要容易得多。

不变项的其他用法

  • 循环不变项
  • 语义不变项

动态合约与代理

通过足够的“能够互相磋商合约、以实现某个目标”的组件和代理,我们也许就能解决软件生产率危机:让软件为我们解决它。

二、死程序不说谎

要崩溃,不要破坏

尽早检测问题的好处之一是你可以更早崩溃。

Java语言中采用了这一哲学,当意料之外的事情在runtime系统中发生时,它会抛出RuntimeException。在别的语言中,你可以做同样的事情。

当你的代码发现,某件被认为不可能发生的事情已经发生,你的程序就不再有存活能力,从此时开始,它所做的所有事情都会变得可疑,所以要尽快终止它。

死程序带来的危害通常比有疾患的程序要小得多。

三、断言式编程

每一个程序员似乎在其职业生涯早期都有一个核心信仰:

这句不可能发生。。。

我们不要这样自我欺骗,特别是在编码时。

如果它不可能发生,用断言确保它不会发生。

无论何时你发现自己在思考“那当然不可能发生”,增加代码检查它。最容易的办法是使用断言。

让断言开着

你的第一条防线是检查任何可能的错误,第二条防线是使用断言设法检测你疏漏的错误。所以尽快断言会带来一些性能开销,也不要关掉它们。

四、何时使用异常

什么是异常情况

关于异常的问题之一是知道何时使用它们。异常很少作为程序的正常流程的一部分使用,异常应保留给意外事件。

比如代试图打开一个文件进行读取,而该文件并不存在,应该引发异常吗?

这取决于实际情况。如果文件应该在那里,那么就返回异常;如果不确定该文件是否存在,就应该返回错误。也就是:

将异常用于处理异常的问题。

如果将异常用于正常处理的一部分的程序,那么就会影响可读性和可维护性,破坏了封装。

错误处理器是另外一种选择

错误处理器是检测到错误时调用的程序。你可以登记一个例程处理特定范畴的错误。处理器会在其中一个错误发生时被调用。

五、怎样配平资源

只要是编程,我们都要管理资源:内存、事务、线程、文件、定时器——所有数量有限的事物。大多数时候,资源使用遵循一种可预测的模式:你分配资源、使用它,然后解除其分配。

但是对于资源的分配和解除许多开发者没有始终如一的计划,所以要做到有始有终:

分配资源的例程也应该释放它。

嵌套的分配

以与资源分配的次序相反的次序解除资源的分配。

在代码的不同地方分配同一组资源时总是以相同的次序分配它们,以避免死锁。

谁分配的资源,就由谁负责该资源分配的解除。

对象与异常

分配与解除分配的对称让人想起类的构造器和析构器。在面向对象语言编程中,把资源封装在类中很有用,需要特定资源的时候实例化该对象;当对象出作用域或被垃圾回收器回收时,则调用析构器解除所包装资源的分配。

无法配平资源

有时基本的资源分配模式并不适用。这通常出现在使用动态数据结构的程序中,一个例程分配一块内存区,并把它链接进某个更大的数据结构中,这块内存可能会在那里呆上一段时间。

这里的诀窍是为内存分配设立一个语义不变项,你需要决定谁为某个聚集数据结构中的数据负责,当你解除顶层结构中的分配时,需要选择:

  1. 顶层结构还负责释放它包含的任何子结构(递归删除)。
  2. 只是解除顶层结构的分配(别处引用被抛弃)。
  3. 如果顶层结构含有任何子结构就拒绝解除自身的分配。

这里的选择取决于每个数据结构自身的情形。

最后,如果追踪资源很棘手,可以通过在动态分配的对象上实现一种引用计数方案,编写自己有限的自动垃圾回收机制。

检查配平

注重实效的程序员对谁也不信任,包括自己,所以构建代码对资源确实得到了适当释放进行实际检查是一个好主意。

学院君

学院君 has written 548 articles

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