Philosophy of Software Design 第一章 介绍(一切都与复杂性有关)

编写软件是人类历史上最纯粹的创造性活动。程序员不受实践限制的约束,比如物理定律;我们可以创造有着现实世界中不可能存在的行为的令人兴奋的虚拟世界。编写程序不像芭蕾或篮球那样要求有很棒的身体技能或协调性。编写程序所需要的是创造性思维以及组织你的想法的能力。如果你可以想象出一个系统,那么你就有可能在电脑程序中把它实现出来。

这表明编写软件时最大的限制是对我们正在创造的系统的理解能力。随着一个程序不停演进并获得更多功能,它会变得复杂,组件之间会有不起眼的依赖。时间流逝,复杂度会累积,程序员在修改系统时,越来越难以把所有相关的因素都记在脑子里。这就会拖慢研发速度,并且导致 bug 产生,然后形成一个恶性循环。任何程序的复杂度都会不可避免的变高。越大型的程序,越多的人参与开发,就越难以控制复杂度。

好的开发工具可以帮助我们处理复杂度,过去的几十年中有许多很厉害的工具被创造了出来。但是仅仅使用这些工具,我们能做到的事情是有限制的。如果我们想要把编写软件变得简单,使得构建强大的系统变得更容易,我们必须找到使软件更简单的方式。尽管我们尽了最大的努力,复杂度还是会随着时间增长,但是更简单的设计允许我们在复杂度不可控之前构建出更大型更强大的系统。

控制复杂度有两种通用的方式,这本书都会讨论。第一种方式使通过使代码更简单更清晰来消除复杂度。比如,通过去除特殊场景或使用一致的标识可以减少复杂性。

第二种方式是封装复杂性,这样程序员可以在不完全了解它的复杂性的情况下基于这个系统进行开发。这种方式被称作 模块化设计。在模块化设计中,一个软件系统划分为不同的模块,比如面向对象语言中的类。模块被设计为相对独立的,这样程序员可以在不了解其他模块细节的情况下开发当前模块。

由于软件是如此具有延展性,所以软件设计是一个会横跨整个软件系统生命周期的持续的过程;这使得软件设计不同于物理系统比如建筑、轮船或桥梁的设计。然而,软件设计并不是一致被这样看待的。对于编程的历史上大多数时候,设计都集中于项目的开始阶段,就像其他工程原则一样。这种方式的极端被称为 瀑布模型,项目被划分为分散的阶段比如需求定义、设计、开发、测试和维护。在瀑布模型中,每个阶段都在下一个阶段开始前结束;大多数情况下,每个阶段是由不同的人来负责。整个系统在设计阶段一次性设计完成。这个阶段结束时设计就固定了,余下的阶段只是充实和设计这个设计。

不幸的是,瀑布模型对软件来说几乎无法奏效。软件系统本质上要比物理系统更复杂;对于大型软件系统来说,在没有搭建任何东西之前,不可能将设计考虑地足够全面。结果,最初的设计会有许多问题。在进行实现之前这些问题都不会明显地暴露出来。然而,这时瀑布模型无法应对重大设计的变更(比如,设计者已经调去其他项目)。因此,程序员会尝试在不改变整体设计的情况下修补问题。这会导致复杂度爆炸性增长。

由于这些问题的存在,当今的大多数软件开发项目采用一种增量的方式,比如 敏捷开发,最初的设计只关注总体功能的一小部分。这一部分被设计、实现,然后被评估。发现并更正原来设计中的问题,然后设计、实现和评估更多的功能。每次迭代都会暴露已有设计中的问题,并在设计下一部分的功能前修复。通过以这样的方式将设计扩散出去,最初设计中的问题可以在系统仍旧比较小型时修复;后面的功能可以从早期功能开发的经验中获益,所以出的问题会变少。

增量的方式对软件奏效是因为软件具有足够的延展性,允许在实现的过程中出现重大的设计变更。相反,对物理系统来说,重大的设计变更会更具有挑战性:比如,在建造过程中修改桥墩的数量是不切实际的。

增量研发意味着软件设计永远未完成。在系统的生命中设计持续存在:开发者应该一直考虑设计问题。增量研发也意味着持续的重新设计。系统或组件的初始设计几乎从来不是最好的;经验会不可避免地展示出完成这件事情的更好方式。作为一名软件开发者,应该始终寻找提升你正在开发的系统的设计的机会,而且应该为设计改进预留出一部分时间。

如果软件开发者应该使用考虑设计问题,并且降低复杂性是软件设计最重要的元素,那么软件开发者应该始终考虑复杂性。这本书就是关于如何使用复杂性指导软件设计的。

这本书有两个整体目标。第一个是描述软件复杂性的天性:“复杂性”意味着什么,为什么重要,以及如何识别出程序中存在不必要的复杂性?这本书第二个,而且更具挑战性的目标是给出在软件开发期间可以用来最小化复杂性的技术。不幸的是,并没有能够保证很棒的软件设计的简单的处方。相反,我会给出一系列哲学化的高层的概念,比如“类应该有深度” 或者 “通过定义使得错误不复存在”。这些概念可能无法立即区分出最好的设计,但是你可以使用它们比较不同的设计选择,并指导你在设计领域的探索。

1.1 如何使用这本书

这里讨论的设计原则很多都在一定程度上抽象,所以不看实际的代码可能会很难理解。寻找既足够小到可以放到书中又足够大到可以展示问题的例子非常困难(如果你遇到了好例子,请把它们发送给我)。所以,学习如何应用这些原则,只看这本书可能不够。

使用这本书最好的方式是和代码审查联合起来。当你阅读别人的代码时,考虑它是否遵循了这里讨论的概念,以及如何影响代码的复杂度。在别人的代码中更容易看到设计问题。你可以用本书中描述的红色警告来识别问题并且提出改进建议。审查代码也会使你接触到新的设计方式以及编程技巧。

提升你的设计技能的最好的方式之一是学习识别 红色警告:一段代码可能比它实际需要的更复杂的信号。在本书的课程中,我会指出和每个主要设计问题相关的表示问题出现的红色警告;最重要的几个在书的最后做了总结。当你编码时可以使用:当你看到一个红色警告,停止工作并寻找可以消除问题的其他设计。当你第一次尝试这种方式时,在找到可以消除红色警告的设计前,可能不得不尝试好几种设计。不要轻易放弃:在修复问题前尝试地越多,你学到的越多。随着时间流逝,你会发现代码中的红色警告越来越少,你的设计变得越来越简洁。你的经验也会向你展示其他红色警告,你可以用来识别设计问题(我会很乐意听到你新发现的这些红色警告)。

当应用本书中的理念时,应当注意节制和谨慎。每条规则都有例外,每个规范都有限制。如果你把每个设计理念都发挥到极致,可能最终会陷入困境。漂亮的设计反应了理念和实现之间较量的平衡。有几个章节中包含名为“过度实践”的部分,描述了如何发现你已经过度了。

这本书几乎所有例子都是 Java 或 C++ 的,大多数讨论都是针对面向对象语言中的设计类。然而,这些理念也可以很好的应用于其他领域。几乎所有和方法有关的理念也可以应用于没有面向对象功能的语言中的函数,比如 C。设计理念也可以应用于模块而不是类,比如子系统或网络服务。

有了这些基础以后,让我们更细致地讨论是什么导致了复杂性,以及如何使软件系统更简单。