• Home

A Philosophy of Software Design

小龙同学

这是本月看的第三本书,也是第四篇读书回顾。

最初注意到这本书大概是18年底在Hacker News看到的一则帖子,里面有一个YouTube链接,是斯坦福大学的教授,也就是本书作者在Google内部的一个演讲,关于软件设计和它刚写的这本书。不懂设计的我,觉得他讲的颇有大师风范,一些观点真是余音绕梁。大概从那时起,我也开始更多地关注设计。当时苦于国内没有这本书,出于对这本书对喜爱购买了原版书。

一年半过去了,中间陆陆续续翻看,最近又过了一遍,回过头来,这本书可以作为是很多同学软件设计的启蒙书。里面提炼的设计原则和典型的设计缺陷,在日常的开发中,以及在很多开源项目里,看别人的代码,就特别能够感受得到。好的设计赏心悦目,解决复杂的问题看起来却比较简单,这里面有很多东西都值得思考和学习。

如何评价代码好坏?以前可能觉得代码写得简洁漂亮,使用了很多流行的技术,完成需求且bug少,现在看起来都不是必要条件,好的设计才是关键。在Google code review的guideline中,列了很多条规则,比如有功能性,测试,文档,代码规范,其中第一条也是最重要的一条:良好的设计。

好了,说了这些,开始聊下本书吧。

在计算机科学中最基本的问题是,问题分解(problem decomposition):如何把复杂的问题拆分成若干个可以独立解决的小问题。这道理几乎每个学过分治算法的同学都懂,但知易行难,设计能力是普通和高质高效程序员的分水岭。

设计的本质是管理(降低)复杂度,这比其他任何东西都重要。复杂的表现有很多,比如代码难以理解,没有测试,关系混乱,改动牵一发而动全身等,作者给复杂下了一个通用的定义:任何让系统难以理解和改动的都可以称之为复杂。具体的表现可以总结为三种:

  1. 改动很多地方
  2. 认知负担
  3. 有很多不清楚的东西

问题间的关联太多,就像一张网一样纷繁复杂,这就造成了1,2;而代码写得太晦涩,难以理解导致了2,3。尽可能地减少问题间的关联关系,简洁易懂,可读性好的代码是解决复杂的关键。

复杂度是可以量化的,作者给出了公式,每个模块本身的复杂度乘以在该模块付出的时间,最后求和。所以从数学的角度,想要降低复杂度,要么降低模块的复杂度,要么减少花费的时间。前者通过良好的设计解决,后者可以将复杂抽象封装起来,比如借助成熟的组件,这样可能就不需要自己去解决了。

作为开发人员,首先心里得有杆秤:只写能用的代码是不够的。原因在于现在的软件开发都是在上一个版本上基础迭代的,我们每次提交的代码,如果设计没有变得更好,那多半就变坏,复杂度会随着迭代一直增加,代码库的质量也就越来越坏。在日常开发中,不应该只关注于完成功能,而是先考虑如何更好的设计。长期来看,带来的收益是巨大的,不仅是个人和项目本身。

说的都是大道理,那么该如何做呢?

首先模块应该被设计成深模块,可以把模块抽象成矩形,面积表示模块提供的功能,而最上的那条边可以认为是模块对外暴露的接口,越短表示接口越简单。什么样的接口才是简单的呢?相当简单的接口和极其复杂的功能。因为这样接口的结果,隐藏了对使用者不重要的信息,那么模块间的依赖就少了,复杂的改动和认知这些复杂度也就少了。这里作者用unix的io函数作为例子,unix仅仅用了5个简单的io函数就实现了复杂的io(底层有硬件的交互,磁盘寻址,系统调用等,很多时候调用者只是想读一个文件成string,仅此而已),对比Java的io,嗯。

+-----------+
|           |
|           |
|           |
+-----------+

(deep module)

+------------------------------+
|                              |
+------------------------------+

(shallow module)

后续的章节都是针对如何设计深模块展开。比如信息的隐藏,避免暴露不重要的信息而增加依赖,否则很多地方都要去了解处理这些信息,这就造成认知负担和改动很多地方。此外,接口的设计应当要简单通用,尽管当前我们只是实现一个具体的功能。不同的层应该有不同的抽象,拿常见的MVC举栗,经常看到不同的层都是直接调用下一层,接口签名几乎都是一样的,这种设计应该保持警惕。接下来就是应该尽可能地将复杂在模块内部处理掉,而不是简单的抛给上层,大多数情况下,底层的模块了解的细节信息更多,内部处理可以使得接口更简单,调用者的心智负担小了也就更开心。再者就是关于模块应该分离还是合并的讨论,总的来说原则就是降低复杂度,从这原则出发,对于需要共享信息,相互依赖,减少重复的,大多数时候会选择合并,这样使得模块的接口更简单。最后一个是错误的处理,关键是需要减少错误处理的地方,避免代码里到处都是抛出异常和错误处理代码,这是编程中我们最不想面对也最容易出错的地方。作者列举了几个方法,包括通过定义某种语义,错误就会不复存在。比如说unsetEnv(key),定义为调用后将不存在key这个变量,如果key不存在,直接返回成功即可,它本来就不存在嘛。这个规则看起来简单,但是实际中经常发现一些接口,会把临界值作为异常返回的。

后续章节是关于如何写可读性好的代码的,可读性好的代码可以降低认知和减少不清楚的地方。代码的可读性好是由读者来评判的,不以作者的意志为转移(这也说明code review 的重要性)。这就要求我们在命名风格,注释上要保持一致性,简单性,这里就不展开说了。

最后还有一点,虽然设计重要,但是也不能过度,要在设计和具体实现间做取舍,不应该时间花费了,取得的收益却很小,甚至是负的。

🏷 design  🏷 book  🏷 review  
© cc-40-by