【书摘】简约之美:软件设计之道

未来的某些事情,是我们所不知道的。


常识

《常识》这本小册子说了什么呢?我随便摘录几句。“如果没有人监督,对国王是不能信任的;或者换句话说,渴望保持专制政权的欲念是君主政体的固有弊病。”“独立自主的问题不外乎意味着:究竟是我们将自己制定我们的法律,还是让这个大陆的目前和将来最大的敌人——英王来吩咐我们,除我所喜欢的法律以外不准有任何法律。”“让我们为宪章加冕,从而使世人知道我们是否赞成君主政体,知道北美的法律就是国王。”

我相信,任何一位读者,只要认真看过全书,都会发现《简约之美》其实只强调了几条互相联系的简单道理:软件是必然要变化的,变化是常态;有变化就需要维护,随着时间的推移,维护成本会远远超过初期开发的成本,占据成本的大头;因此,在软件开发中,最重要的是要降低维护成本;维护成本正比于系统的复杂程度,所以要降低维护成本,系统的设计就应当追求简单清晰。

1.1 计算机出了什么问题?

编程所要用到的艺术和才能——化繁为简。

第2章 缺失的科学

软件系统中任何与架构有关的技术决策,以及在开发系统中所做的技术决策,都可以归到“软件设计”的范畴里。

2.1 程序员也是设计师

每个写代码的人都是设计师,团队里的每个人都有责任保证自己的代码有着良好的设计。任何软件项目里,任何写代码的人,在任何层面上,都不能忽略软件设计。

所有的开发人员都应有权在自己的工作中做出良好的设计决策

身为设计师,必须时时愿意聆听建议和反馈,因为程序员大都比较聪明,有不错的想法。但是考虑了所有这些建议和反馈之后,任何决策都必须由单独的个人而不是一群人来做出。

2.2 软件设计的科学

科学必须包含汇总而来的知识。也就是,它必须包含事实而不是意见,且这些事实必须汇总起来(比如集结成书)。

这些知识必须具有某种结构。知识必须能分类,其中的各个部分必须能够依据重要性之类的指标,妥善建立起与其他部分的联系。

科学必须包括一般性的事实或者基本的规则。

科学必须告诉你在现实世界中如何做一些事情。它必须能够应用到工作或生活中。

通常,科学是经由科学方法来发现或证明的。科学方法必须观察现实世界,提炼出关于现实世界的理论,通过验证理论,而且这些实验必须是可重复的。这样才能说明理论是普适的真理,而不仅仅是巧合或者特例。

定义告诉你事物是什么,应当如何使用。

事实是关于事物的真实陈述。每一点真实的信息都是事实。

条例是给你的确切建议,它包含某些具体的信息,用于制订决策。但是,条例并不能帮你绝对准确地预测未来,也不能帮你发现其他真理。它们通常会告诉你是否需要采取某些行动。

规则是永远为真的事实,它涵盖了很多领域的知识。它们帮你发现其他重要的真理,帮你预测未来要发生的事情。

软件设计是有章(规则)可循的,它们可以被认识,可以被理解。规则是永恒不变的,是基本的事实,而且确实可行。

2.3 为什么不存在软件设计科学

软件复杂性的主要根源就在于缺乏科学。如果程序员有科学指导,知道该如何开发简单的软件,就不会有这么多的复杂性问题,我们也不需要运用令人发狂的过程来管理这些复杂性。

第3章 软件设计的推动力

全部软件都有一个相同的目标: 帮助其他人。

不理解“帮助其他人”的程序员,只能写出糟糕的程序,也就是说,他们的程序提供不了什么帮助。实际上,大概存在这么一点理论(根据对大量程序员的长时间观察所得的猜想):一个人写出优秀软件的潜力,完全取决于他在多大程度上理解了“帮助其他人”的思想。

总之,在设计软件时,应当将目标——帮助他人——视为应该考虑的最重要因素,这样,我们才能认识并了解软件设计的真正科学。

从目标看,我们知道开发软件是为了帮助其他人。所以,软件设计科学的目标应该是: 确保软件能提供尽可能多的帮助。其次,我们通常希望软件可以给大家提供持续的帮助。所以,第二个目标是: 确保软件能持续提供尽可能多的帮助。

设计程序员能尽可能简单地开发和维护的软件系统,这样的系统才能为用户提供尽可能多的帮助,而且能持续提供尽可能多的帮助。

4.1 软件设计的方程式

任何一点改变,其合意程度与其价值成正比,与所付出的成本成反比。

4.1.1 价值

价值由两部分组成:可能价值(这个变化有多大可能帮到用户)、潜在价值(这个变化在对用户提供帮助的时候,将为用户提供多大的帮助)。

4.1.3 维护

成本包含实现成本和维护成本,价值也包括当前价值和未来价值。

4.1.4 完整的方程式

改变的合意程度(可行性),正比于软件当前价值与未来价值之和,反比于实现成本和维护成本之和。

4.1.6 你需要什么,不需要什么

相比降低实现成本,降低维护成本更加重要。 在软件设计的科学中,这是非常重要的一点。

4.2 设计的质量

设计的质量好坏,正比于该系统在未来能持续帮助他人时间的长度。

把自己禁锢在某种工作定势里,要保持灵活;不要做任何以后无法改变的决策;在设计时要慎重,慎重,再慎重。

4.3 不可预测的结果

程序员犯的最常见也是最严重的错误,就是在其实不知道未来的时候去预测未来。

重要的是要记住,存在着未来。但是,这不要求你必须预测未来,它只是说明你为什么应当遵循本书的规则和条例来决策——因为无论未来会发生什么,它们总可以保证你的软件不偏离正轨。

第5章 变化

我们得到了变化定律(Law of Change): 程序存在的时间越久,它的某个部分需要变化的可能性就越高。

关键在于,你并不需要去预测什么会变化,你需要知道的是,变化必然会发生。程序应该保证尽可能合理的灵活性,这样,不管未来发生什么变化,都可以应付得了

5.2 软件设计的三大误区

为了适应变化定律,软件设计师常常会掉进误区。其中有3个误区最常见,这里按照其发生频率逐一列出来: (1)编写不必要的代码 (2)代码难以修改(3)过分追求通用

5.2.1 编写不必要的代码

在需求来临之前就编写代码的另一点危险是,当前用不到的代码很可能会导致“劣化”。因为代码从来没有用到,它很可能与系统的其他部分脱节,继而产生bug,可是你永远也不知道。最终等你想去使用的时候,就得花时间去排错。更糟糕的是,你很可能相信这些从来没用过的代码是正确的而忽略了检查,这样bug就留给了用户。其实,这条规则应当这样展开: 不要编写不是必需的代码,并且要删除没有用到的代码。

5.2.2 代码难以修改

软件项目的一大杀手就是所谓的“僵化设计”(rigid design)。也就是说,程序员写出来的代码很难修改。僵化设计有两大原因: (1)对未来做太多假设(2)不仔细设计就编写代码

要避免僵化设计,就应当做到: 设计程序时,应当根据你现在确切知道的需求,而不是你认为未来会出现的需求。

5.2.3 过分追求通用

总的来说,如果你的设计让事情更复杂而不是变简单,就是在做过度工程。如果你只需要清理蚁穴,动用轨道激光就会把事情大大搞复杂,其实只用一点蚂蚁药就可以解决问题(假设它有效)。

在追求通用时,应当选择正确的事情,选择正确的方法,这是成功的软件设计的基础。然而,太过通用,会带来说不完的复杂和混乱,也会大大抬高维护成本。避免此误区的办法,和避免僵化设计的一样: 仅仅根据目前确知的需求来考虑通用。

5.3 渐进式开发及设计

有个办法可从根本上避免这三大误区,这就是“渐进式开发和设计”。它要求按照特定顺序,一点一点地设计和构建系统。

显然,精益混合了两种做法:一种叫做“渐进开发”,另一种叫做“渐进设计”。渐进开发是一种通过小步骤构建整个系统的办法。在上面的步骤里,“实现”开头的每一步都是渐进开发过程的一部分。渐进设计是一种类似的方法,它也通过一系列小步骤用来创建和改进系统的设计。在上面的步骤里,以“修改”或“设计”开头的每一步,都是渐进设计过程的一部分。渐进开发和设计并不是唯一有效的软件开发方法,但是它无疑可以避免之前列出的三大误区。

第6章 缺陷与设计

缺陷概率定律”: 在程序中新增缺陷的可能性与代码修改量成正比。

最好的设计,就是能适应外界尽可能多的变化,而软件自身的变化要尽可能少。

6.1 如果这不是问题……

永远不要“修正”任何东西,除非它真的有问题,而且有证据表明问题确实存在。

6.2 避免重复

在软件设计中,这或许是最著名的条例。其他资料也曾提过该条例,鉴于它的重要性,我们在这里重申: 理想情况下,任何系统里的任何信息,都应当只存在一次。

众多优秀设计都基于这一规律。也就是说,你能更聪明地让代码“使用”其他代码,把信息集中运用好,那么设计也就更好。在这一领域,你同样可以真正用自己的聪明才智为编程创造价值。

第7章 简洁

这就是简洁定律(Law of Simplicity): 软件任何一部分的维护难度,反比于该部分的简洁程度。

7.4 保持一致

如果代码不能保持一致,程序员理解和阅读起来都要更加困难。

编程也是这样——缺乏一致性,只会一团糟。有了一致性,世界就很简单。即便你做不到那么简单,至少也要做到:一旦你理解了某种复杂性,就不必再进行重复劳动。

7.5 可读性

代码可读性主要取决于字母和符号之间的空白排布。

7.5.1 命名

可读性的另一部分重要内容是为变量、函数、类等选择合适的名字,理想的命名应该这样: 名字应当足够长,能够完整表达其意义或描述其功能,但不能太长,以免影响阅读。

7.5.2 注释

保证代码的可读性,好的的注释也很重要。但是,代码的意图通常不应该用注释来说明,直接阅读代码就应当能够理解。如果发现意图不够明显,那么就说明这段代码还可以变得更简单。如果你的代码实在不能更简单,才应该写注释来说明。

注释的真实目的,是在理由不够清晰明显时加以解释。如果不解释,其他程序员在修改这段代码时可能会很困惑;如果不明白这些理由,他们可能会删改其中重要的部分。

第8章 复杂性

有些项目从一启动就设定了繁多的需求,所以永远无法发布第一版。如果遇到这种情况,就应当删减功能。初次发布不应当设定过高的目标,而应当先让程序跑起来,再持续改进。

除了新增功能,其他一些做法也会增加复杂性,以下列出了最常见的做法。

  1. 扩展软件的用途 一般情况下,应当绝对禁止这样做。市场部可能巴望着某款软件既能够计算个税,又可以充当菜谱,这样的需求,你必须尽全力抵制。软件应当坚守已经确定的用途,只要妥善完成这些目标,你就会获得成功(前提是该软件能帮到用户,切实满足其需求)。
  2. 新增程序员 没错,往团队里增加新人并不会让事情变简单,相反会更复杂。Fred Brooks的名作《人月神话》说的就是这个道理。如果已经有了10个开发人员,再增加1个人,就意味着需要为他设定合适的岗位,花时间让之前的10个人适应新人,花时间让新人学会与那10个人沟通,如此等等。相比众多平庸的开发人员,少量精干的开发人员更容易获得成功。
  3. 做无谓的改变 每做一点改变,都会增加复杂性。无论是需求变化、设计变化,或是只修改某段代码,都有可能增加新的bug,另外别忘了算上决定如何变化所需的时间,实现它所需的时间,验证它是否影响到原有系统所需的时间,记录它的时间,测试它的时间。每做一点新变化,整体复杂性就会增加一点,所以变化越多,每个变化要花的时间就越长。做出某些变化是重要的,但是应当谨慎决策,而不是一拍脑瓜就定了。
  4. 困于糟糕的技术 一般来说,“困于糟糕的技术”指的是你之前决定了采用某种技术,因为极度依赖它,长期无法摆脱。这里说的“糟糕”,意思是你深陷其中(未来无法简单地切换到其他技术),不能灵活地适应未来的需求,或是达不到设计简洁软件所需的质量标准。
  5. 理解错误 程序员不理解自己的工作,就容易设计出复杂的系统。这可能是恶性循环:理解错误导致复杂性,复杂性又进一步加剧理解错误,如此往复。提升设计水平的最主要办法是,确保自己完全理解所用的系统和工具。你对它们的理解越到位,对软件开发的一般规律了解越多,你的设计就越简洁。
  6. 糟糕的设计或不做设计 一般来说,它指的是“没有为变化做计划”。万物都是会变化的,项目增长时,设计仍然要维持简单。你必须一开始就做好设计,而且在系统膨胀时不断进行优秀的设计;否则,复杂性就会迅速增长,因为如果设计得不好,每项功能都会让代码加倍复杂,而不是只复杂一点点。
  7. 重新发明轮子 如果有相当不错的现成协议,还要自己发明协议,那么仅仅为了把软件跑起来,这些协议也会花去你大量的时间。决不要什么都靠自力更生,去自己开发什么Web服务器、协议或者重要的类库,除非它们是你的最终产品。只有在满足以下任何一个条件的前提下,重新发明轮子才有价值: (1)你需要的东西不存在;(2)现有的各种“轮子”都很糟糕,会把你困住; (3)现有的“轮子”根本无法满足你的需求;(4)现有的“轮子”缺乏良好的维护,而你也不能接过维护的任务(比如,你没有源代码)。

8.2 糟糕的技术

出现复杂性的另一个常见原因就是,系统里选择了错误的技术,尤其是最终发现并不能很好适应未来需求的技术。但是既然无法预测未来,现在就决定要选择什么技术并不简单。好在,开始使用之前,你可以通过三个因素来判断技术是否“糟糕”:生存潜力、互通性、对品质的重视。

8.4 复杂问题

如果你在解决复杂问题时遇到了麻烦,那么用简单易懂的文字把它写在纸上,或者画出来。有些最优秀的程序设计就是在纸上完成的,真的。把它输入到计算机里只是次要的细节。 大多数麻烦的设计问题,都可以用在纸上画图或写出来的办法找到答案。

8.6 推倒重来

如果下面的条件全都满足,你才应该重写。

  1. 你已经完成了准确评估,证明重写整个系统会比重新设计现有系统更有效率。只有猜测是不够的,你需要真正去做一些重新设计现有系统的试验,然后对比结果。已有的复杂系统可能很难应付,某些部分可能很难处理,但是为了知道修复它需要多少时间,你必须动手做一些尝试。
  2. 你有足够的时间用来开发新的系统。
  3. 你要比原有系统的设计师更高明,或者,如果原有系统是你设计的,但现在你的设计能力已经大大提升了。
  4. 你完全打算好了通过一系列简单的步骤设计整个新系统,在每一步都有用户提供反馈。
  5. 你有足够的资源,可兼顾维护原有系统和重新设计系统。绝对不要为了让程序员重写新系统而停止对原有系统的维护。系统只要在使用,都离不开维护。请记住,你自己的精力也是一种资源,必须慎重分配——如果两线作战,你每天有足够的时间分配给原有系统和新系统吗?

如果上面的条件都满足,那么推倒重来是可以接受的。否则,应该做的事情不是推倒重来,而是降低现有系统的复杂性,也就是通过一系列简单步骤来改进系统的设计。

第9章 测试

测试法则(Law of Testing)告诉我们: 你对软件行为的了解程度,等于你真正测试它的程度。

总的来说,你可以这么理解: 除非亲自测试过,否则你不知道软件是否能正常运行。

附录A 软件设计的规则

  • 变化定律:程序存在的时间越久,它的某个部分需要变化的可能性越大。
  • 缺陷定律:在程序中新增缺陷的可能性与代码修改量成正比。
  • 简洁定律:软件任何一部分的维护难度,反比于该部分的简洁程度。
  • 测试定律:你对软件行为的了解程度,等于你真正测试它的程度。

如果你希望把最重要的事实综合成软件设计时要记得的两句话,就是:

  • 相比降低开发成本,降低维护成本更加重要。 维护成本正比于系统的复杂程度。
  • 有了这两条,以及对软件目的的了解,再加上你知道整个系统的复杂性源自各部分的复杂性,你就有相当的把握去重新认识软件设计这整门科学。

附录B 事实、规则、条例、定义

本附录列出了本书所涵盖的各条重要的事实、规则、条例、定义。

事实:好程序员和差程序员的差别就在于理解能力。差劲的程序员不理解自己做的事情,优秀的程序员则相反。

条例:“好程序员”应当竭尽全力,把程序写得让其他程序员容易理解。 定义:程序就是 给计算机的一系列指令; 计算机依据指令进行的操作;

定义:软件系统中任何与架构有关的技术决策,以及在开发系统中所做的技术决策,都可以归到“软件设计”的范畴里。 事实:每个写代码的人都是设计师。

事实:设计与民主无关,它应当由个人完成。 事实:软件设计是有章(规则)可循的,它们可以被认识,可以被理解。规则是恒久不变的,是基本的事实,而且确实可行。

规则:软件的目的就是帮助其他人。 事实:软件设计的目的如下。 确保软件能提供尽可能多的帮助; 确保软件能持续提供尽可能多的帮助;

设计程序员能尽可能简单地开发和维护的软件系统,这样的系统才能为用户提供尽可能多的帮助,而且能持续提供尽可能多的帮助。

这说明,降低维护成本比降低开发成本更重要。

  • 条例:要做多少设计,应当正比于未来软件能够持续为人们提供帮助的时间的长度。
  • 条例:未来的某些事情,是我们所不知道的。
  • 事实:程序员犯的最常见也是最严重的错误,就是在其实不知道未来的时候去预测未来。
  • 条例:最安全的情况是,完全不尝试预测未来,所有的设计决策都应当根据当前确切知道的信息来做。
  • 条例:变化定律:程序存在的时间越久,它的某个部分需要变化的可能性越大。 事实:在落实变化法则时,软件设计师容易犯的三个错误(也就是本书中的“三大缺陷”)是:
  • 编写不必要的代码 代码难以修改 过分追求通用 条例:直到真正要用了才编写代码,清理掉用不到的代码。
  • 条例:代码的设计基础,应当是目前所知的信息,而不是你认为未来要发生的情况。 事实:如果设计让事情更复杂,而不是更简单,就犯了过度工程的错误。
  • 条例:在考虑通用时,只需要考虑当前的通用需求。 条例:采用渐进式开发和设计,可以避免三大缺陷。
  • 条例:缺陷概率法则:在程序新增缺陷的可能性与代码修改量成正比。 条例:最好的设计,就是能适应外界尽可能多的变化,而软件自身的变化要尽可能少。
  • 条例:永远不要“修正”任何东西,除非它真的有问题,而且有证据表明问题确实存在。 条例:理想情况下,任何系统里的任何信息,都应当只存在一次。
  • 规则:简洁定律:软件任何一部分的维护难度,反比于该部分的简洁程度。 事实:简洁是相对的。 条例:如果你真的希望成功,最好是把产品简化到傻子也能懂。
  • 条例:要保持一致。 条例:代码可读性主要取决于字母和符号之间的空白排布。 条例:名字应当足够长,能够完整表达其意义或描述其功能,但不能太长,以免影响阅读。
  • 条例:代码应当解释程序为什么这么做,而不是它在做什么。
  • 条例:简洁离不开设计。
  • 条例:你可以这样增加复杂性: 扩展软件的用途; 新增程序员; 做无谓的改变; 困于糟糕的技术; 理解错误; 糟糕的设计或者不做设计;重新发明轮子; 背离软件原来的用途; 条例:可以通过考察生存潜力、互通性、对品质的重视,判断某种技术是否“糟糕”。
  • 条例:通常,如果某件事情变得非常复杂,也就意味着深藏在表面的复杂之下,设计出了问题。 条例:在复杂性面前,问问自己“真正要解决的问题是什么”。
  • 条例:大多数麻烦的设计问题,都可以用在纸上画图或写出来的办法找到答案。 条例:要应付系统中的复杂性,可以将系统分解成独立的小部分,逐步重新设计。
  • 事实:所有可行的简化,其核心问题都是:怎么做,才可以让事情处理或是理解起来更容易。
  • 条例:如果遇到不可解决的复杂性,在程序外面妥善包装上一层,让其他程序员更容易使用和理解。 条例:推倒重来只有在一些非常有限的情况下才是可以接受的。
  • 规则:测试定律:你对软件行为的了解程度,等于你真正测试它的程度。 条例:除非亲自测试过,否则你不知道软件是否能正常运行。