第 1 章 介绍
(一切都围绕复杂性)
编写计算机软件是人类历史上最纯粹的创作活动之一。程序员不受物理定律等现实条件的约束,可以创造出令人兴奋的虚拟世界,让其中的事物以现实世界中不可能的方式运行。编程不像芭蕾或篮球那样,需要高超的身体技巧或协调能力。编程所需的,只是富有创造力的头脑和组织思路的能力。如果你能在脑中构想出一个系统,很可能就能用计算机程序实现它。
本章概览这本书要解决什么问题?
这意味着,编写软件的最大限制在于我们理解所创建系统的能力。随着程序不断演进、功能不断增加,程序会变得越来越复杂,组件之间也会产生微妙的依赖关系。复杂性日积月累,程序员在修改系统时越来越难以顾及所有相关因素。这会减慢开发速度并导致代码缺陷,而缺陷又会进一步拖慢开发、增加成本。在任何程序的生命周期中,复杂性都不可避免地会增加。程序越大,参与开发的人越多,管理复杂性就越困难。
好的开发工具可以帮助我们应对复杂性,过去几十年也涌现了许多出色的工具。但仅靠工具,能做的终究有限。要让软件更容易编写,以更低的成本构建功能更强大的系统,就必须找到简化软件的方法。即使竭尽全力,复杂性仍会随着时间推移而增加;不过,更简单的设计能让我们在复杂性变得难以驾驭之前,构建出更大、功能更强大的系统。
有两种解决复杂性的通用方法,这两种方法都将在本书中进行讨论。第一种方法是通过使代码更简单和更易理解来消除复杂性。例如,可以通过消除特殊情况或以一致的方式使用标识符来降低复杂性。
应对复杂性的第二种方法是将其封装起来,让程序员在开发系统时不必同时面对全部复杂性。这种方法称为模块化设计。在模块化设计中,软件系统被划分为模块,例如面向对象语言中的类。各模块在设计上相对独立,程序员可以开发其中一个模块,而不必了解其他模块的细节。
译者图解消除规则,还是隔离规则?
软件易于改变,因此软件设计是一个贯穿系统整个生命周期的持续过程。这一点不同于建筑物、船舶或桥梁等物理系统的设计。但人们并非一直这样看待软件设计。在编程的早期,设计往往集中在项目开始时进行,与其他工程学科一样。这种做法的极端形式称为瀑布式模型:项目被划分为需求定义、设计、编码、测试和维护等彼此分明的阶段。每个阶段完成后,下一个阶段才会开始;许多项目还会由不同的人负责各个阶段。整个系统在设计阶段一次性设计完毕,随后冻结设计,后续阶段则负责细化并实现它。
不幸的是,瀑布式模型很少能在软件开发中取得良好效果。软件系统本身比物理系统更复杂。在尚未动手构建时,不可能把大型软件系统的设计构想得足够周全,预先看清其中的所有影响。因此,初始设计会存在许多问题,往往要到实现工作推进了相当一段时间后才会显现。但瀑布式模型并不便于在此时作出重大设计变更(例如,设计人员可能已转到其他项目)。于是,开发人员设法在不改变整体设计的前提下修修补补,导致复杂性急剧增加。
由于这些问题,当今大多数软件开发项目采用敏捷开发等增量方法,初始设计只关注整体功能的一小部分。先设计、实现并评估这一小部分,找出并修正设计中的问题,再设计、实现和评估更多功能。每次迭代都会暴露现有设计的问题,并在设计下一批功能前解决这些问题。把设计工作分散到整个开发过程中,就能在系统还很小时修正初始设计的问题。后续功能可以借鉴实现早期功能时积累的经验,因此问题也会较少。
增量方法适用于软件,因为软件足够灵活,允许在实现过程中进行重大设计变更。相比之下,物理系统的重大设计变更要困难得多:例如,在施工中途改变支撑桥梁的塔架数量就不切实际。
增量开发意味着软件设计永无止境。设计贯穿系统的整个生命周期,开发人员应始终思考设计问题。增量开发也意味着持续重新设计。系统或组件的初始设计几乎从来都不是最佳设计;随着经验积累,总会发现更好的做法。作为软件开发人员,你应不断寻找改进系统设计的机会,并在计划中为此留出一部分时间。
如果软件开发人员应始终考虑设计问题,而降低复杂性是软件设计中最重要的要素,则软件开发人员应始终考虑复杂性。这本书就是关于如何使用复杂性来指导软件设计的整个生命周期。
本书有两个总体目标。首先是说明软件复杂性的本质:“复杂性”是什么意思,为什么重要,又该如何识别程序中不必要的复杂性?第二个目标更具挑战性:介绍在软件开发过程中尽量减少复杂性的技术。遗憾的是,没有简单的诀窍能保证做出出色的软件设计。因此,我会提出一些较高层级、近乎哲学的概念,例如“类应该是深的”或“通过定义来规避错误”。这些概念未必能让你立即找到最佳设计,却能帮助你比较备选方案,探索可能的设计。
1.1 如何使用这本书
本书中的许多设计原则比较抽象,不结合实际代码,可能很难体会。示例既要足够小,能放进书中,又要足够大,能说明真实系统中的问题,这样的例子并不好找(如果遇到好的示例,请发给我)。因此,仅靠阅读本书,可能还不足以学会应用这些原则。
使用本书的最佳方式是结合代码审查。阅读别人的代码时,想一想它是否符合本书讨论的理念,这又如何影响代码的复杂性。相比自己的代码,别人的代码中有哪些设计问题,往往更容易看出来。你可以借助书中的危险信号发现问题并提出改进建议。代码审查也能让你接触新的设计方法和编程技术。
提高设计能力的最佳方法之一,是学会识别危险信号:它们表明一段代码可能存在不必要的复杂性。本书会围绕各个主要设计问题指出相应的危险信号,并在书末汇总最重要的一些。编码时可以利用这些信号:一旦发现,就停下来寻找能消除问题的其他设计。刚开始尝试时,你可能需要比较几个方案,才能找到消除危险信号的办法。不要轻易放弃:在解决问题之前尝试的方案越多,学到的也越多。渐渐地,你会发现代码中的危险信号越来越少,设计也越来越整洁。你还会从自己的经验中发现其他有助于识别设计问题的危险信号(我很乐意听到这些)。
应用本书中的思想时,务必把握分寸、审慎判断。每条规则都有例外,每项原则都有局限。把任何设计理念推向极端,都可能适得其反。好的设计体现了不同思想和方法之间的平衡。书中若干章设有“做过头了”一节,介绍如何识别自己是否把一种好做法用过了头。
本书几乎所有示例都使用 Java 或 C++ 编写,大部分讨论也围绕如何在面向对象语言中设计类展开。但这些思想同样适用于其他领域。几乎所有关于方法的思想,都可以用于没有面向对象特性的语言中的函数,例如 C 语言的函数。这些设计思想也适用于类以外的模块,例如子系统或网络服务。
在这种背景下,让我们详细讨论导致复杂性的原因以及如何简化软件系统。