自从软件危机的概念被提出以来,人们就在不断地探索解决之道。期间,这些探索者们从其他如硬件、建筑等相对成熟的行业借鉴了不少经验和知识,希望能够以工程化的方法解决软件领域所面对的难题,并提出了“软件工程”这样一个知识框架用以指导实践。但是,几十年过去了,结果表明,“软件工程”为我们带来的对软件开发本身反思方面的作用要远远大于解决软件危机方面的作用。

    经历了这么长时间不那么成功的“软件工程”经历的人们不禁要问:为何在其他领域屡获成功的工程化经验,对于软件领域就不起作用了呢?Richard P. Gabriel在其“Patterns Of Software”一书中的一句话道出了最根本的原因:“…… 我们谈论了太多的关于软件的东西,对于软件这个东西本身,关注得太少了……”。确实是这样,我们从其他领域借鉴了太多的形式的东西,而对于软件以及软件开发活动本身的规律的认识上却思考不多。而工程化的根本就在于契合事物本身的规律。

    关于软件本身的特点和规律,Jack W. Reeves在其著名的论文“What Is Software Design?”中有深刻的论述。在此,仅列出几个重要的结论:
? * 软件的设计成本非常低,因此很容易变得极其复杂
? * 软件的构建成本基本为零,因此先构建出来然后验证是一种经济的做法
? * 软件不受任何物理规律的约束,因此可能出现的故障情况比其他领域要高得多。
? * 软件是非常精确的,一个bit的错误都会导致整个系统的崩溃。
    这些规律对软件开发有着根本性的影响,主要表现在:需求范围的限定、设计成本的控制,需求满足性的验证以及软件产品本身的健康演进。

    在我们实际的软件开发中,最令人头疼的问题莫过于两点:1、时间有限,要实现的功能太多;2、bug层出不穷,好像永远也改不完。一般来讲,造成这两个问题的根源都是由于违背了上述的软件本身的规律。下面我们来简单分析一下。

    我们的需求分析结果是以软件需求规格说明书的形式交付的,描述的语言是自然语言。自然语言往往是含糊地。虽然对人来讲觉得可能已经很清楚了,但是对于真正去执行的计算机来说,精确度远远不够。因此,需求和实现之间存在着许多“不那么明确”的地方。由于软件的设计成本很低,因此,“负责任”开发人员在设计和实现时,会不经意地把这些不明确的地方最大化,造成需求和软件开发成本的隐式膨胀。如果我们要求每增加一个设计元素,那么设计开发人员就要去爬十层楼,相信我们的软件会减掉不少“赘肉”:)

    此外,大家想想,我们的软件中的bug都是在什么阶段大量涌现的?没错,是联调阶段。正是由于软件的构建成本很低,因此,我们才得以在前期不那么有纪律地进行编码,然后把软件构建出来进行调试,验证。把软件先构建出来再验证本身没什么问题,问题在于由于软件不受任何物理规律约束,并且非常精确,因此任何一丁点的修改都可能造成难以预料的严重后果。而我们也缺乏一种有效的检查修改所造成影响的手段。测试部门的人工测试或者基于界面的robot测试非常的低效且容易遗漏。

    那么我们怎么做才能符合软件本身的规律,从而在最大程度上避免上述问题呢?我们需要用和实现语言同样精确的语言来描述需求;我们需要用和实现语言同样精确的语言来描述验收条件;我们需要足够细粒度的检查点来固定我们的软件;这些检查点的运行要足够快、成本足够低以便于我们可以经常性的运行它们。这意味着什么呢?这意味着开发人员在编写实现代码前,要先用同样的语言编写测试,用测试代码来表达需求和验收条件,用测试来驱动整个的开发过程。这样做能有效地解决上述问题吗?我们来分析一下。

    软件是精确地,容不得半点含糊。因此用编程语言描述需求可以避免那些似是而非的情况,如果觉得自己很清楚一个需求,那么请用编程语言写出一个测试用例,精确地表达需求场景和验收条件,如果写不出来,那就表明还没有搞清楚。此外,有了这个可以用编程语言描述的验收条件,就有了准确的判断完成的标准。一切以完成验收条件为目标,这样也就避免了因为没有精确目标而随手编写多余的代码。

    另一方面,如果用测试来驱动我们的开发,那么每一个测试用例就充当了用以固定软件行为的检查点的角色。随着功能的不断增加,这些测试用例不断地累积,形成了一张坚固的安全网络。每当我们增加新功能时;每当我们更改bug时;每当我们重构代码时;我们都可以即时地运行这些测试,然后就可以立即得到反馈。有了这些检查点,一旦发现问题,可以很快地进行定位:问题一定出在上一次正常运行和这一次故障运行之间的修改上面,我们的测试用例的粒度越小,运行得越频繁,问题定位起来就越容易。

    如果用测试来驱动开发,那么我们写出的代码将天生就具有可测试性。而可测试性是好的设计的重要标志。我们的实现代码将完全可以脱离预先设定的环境进行测试,测试的粒度完全可以根据需要进行调整。和完整的系统测试相比,这样的测试成本更低、运行更快、更灵活,更加可控,它们也是持续集成的重要基石。

    软件开发就像是攀岩,既充满乐趣,又深具挑战,有时甚至危险重重。正如攀岩时最安全有效的方法是保证我们的四肢中每次仅移动有一个一样,我们在软件开发中也要遵守这样的纪律。在软件开发活动中,我们的四肢是什么呢?它们分别是:编写测试代码;编写实现代码;修改测试代码;修改实现代码。我们要非常清楚地知道我们正在移动得是四肢中的哪一个,并严格按照纪律执行。