413 - 《读书笔记:A Philosophy of Software Design》

发布于 2024年2月17日

推荐一本 2018 年的书《A Philosophy of Software Design》,豆瓣评分 9.2,告诉你如何做更好的设计,写更好的代码。年前翻完的,收获挺大,推荐精读。我用 ChatGPT 4 Turbo 翻译了一版中英对照的,也可以看社区同学两年前翻译的版本,https://github.com/Cactus-proj/A-Philosophy-of-Software-Design-zh

以下是笔记。

1、编写软件时,随着时间的推移,复杂性逐渐累积,程序员在修改系统时越来越难以将所有相关因素保持在脑海中。这减慢了开发速度,导致了错误,这些错误进一步减慢了开发速度并增加了成本。(开发 Umi 时其实还好,在开发 Mako 时就深有体会了,因为构建工具涉及的面太广,很难凭记忆把事情全记到脑子里,我们的做法是写详细的 RFC,忘记了就回头去看 RFC 回顾下)。这本书讨论的就是如何应对复杂性,通常有两种方法,1)简化代码使之更明了,2)封装代码让我们不一次性暴露所有复杂性。(对于前端来说,npm 包、组件库等都属于后者)

2、项目开发方式典型的有几种,1)瀑布模型,2)敏捷模型。瀑布模型在每个阶段完成后才开始下一个阶段,整个系统在设计阶段一次性设计完成;敏捷模型初始设计专注于整体功能的一小部分,这一部分被设计、实现,然后进行评估,然后每次迭代都会暴露出现有设计的问题,在设计下一批功能之前进行修复。(显然,后者是更适合大型软件开发的,项目大了之后根本不可能一次性设计清楚。)

3、复杂性是什么?复杂性是指与软件系统的结构相关的任何事物,这些事物使得理解和修改系统变得困难。复杂性有多种形式,比如,1)很难理解一段代码的工作原理,2)实现一个小改进可能需要大量努力,或者可能不清楚需要修改系统的哪些部分才能做出改进,3)修复一个错误而不引入另一个错误是困难的。所以,如果一个软件系统难以理解和修改,那么它就是复杂的,反之就是简单的。此外,复杂度还分人,自己能轻松工作还不够,得让其他人也能轻松工作。

4、怎么识别复杂性?复杂性有三种表现,1)变更放大,一个看似简单的变更需要在许多不同的地方修改代码,(比如每年需要更新的 copyright 年份,你的项目是改 N 份、1 份还是自动更新?),2)认知负荷,指的是开发者为了完成一个任务需要知道多少信息(代码行数多少不能代表复杂性,有时候更多的代码反而更容易理解,比如 eslint 有个规则是 no-nested-ternary,嵌套的三元表达式通常代码少但难以理解),3)未知的未知数,开发者不清楚需要修改哪些代码片段才能完成任务,或者不清楚需要掌握哪些信息才能完成任务,(这是最麻烦的,你不清楚你不知道什么,直到变更之后,bug 出现了,你才会发现他)。

5、为啥会有复杂性?两个原因,1)依赖性,当给定的代码片段不能被孤立地理解和修改时,就存在依赖性,2)晦涩性,到当重要信息不明显时,就会导致晦涩,比如太通用变量名 time、同一个变量两个含义的不一致性、文档不充分等。依赖性导致变更放大和高认知负荷。晦涩性造成未知的未知,并且也增加了认知负荷。

6、复杂性是递增的。复杂性之所以产生,是因为成百上千的小依赖和不明确性随着时间的推移而积累起来。最终,这些小问题如此之多,以至于系统的每一个可能的变更都会受到其中几个问题的影响。你很容易说服自己,当前变更引入的一点点复杂性没什么大不了的。然而,如果每个开发者都对每次变更采取这种态度,复杂性就会迅速积累。一旦复杂性积累起来,就很难消除,因为单独修复一个依赖关系或不明确性本身不会产生太大的影响。为了减缓复杂性的增长,你必须采纳“零容忍”哲学。

7、编程心态有两种,战术编程和战略编程。1)战术编程是能用就行,其目标是让东西跑起来,其缺点是会导致复杂性迅速累积,特别是当每个人都在进行战术性编程时。团队中通常都会有一些「战术龙卷风队员」,他们的代码输出速度远超他人,能快速实现一个功能,一些组织会视他们为英雄,然而,龙卷风过后是一片破坏的结果。2)战略编程是意识到仅有可工作的代码是不够的,为了更快完成当前任务而引入不必要的复杂性是不可接受的,其目标是产出好的设计,而这个设计恰好也能工作。(用哪种心态编程应该也需要 by scene,脚本类的、营销类的、生命周期短的、创业类的,用战术编程可能更合适。)

🚩 注意「战术编程」。

8、模块化设计和深度模块。模块化设计让开发者在任何给定时间只需要面对整体复杂性的一小部分。一些注意点,1)模块分为两部分:接口和实现,开发者不应该需要理解他或她正在工作的模块之外的其他模块的实现,2)最好的模块是那些接口比实现简单得多的模块,3)抽象是对实体的简化视图,省略了不重要的细节,他让我们更容易思考和操作复杂的事物,抽象过程中,「不重要」是关键,从抽象中省略的不重要细节越多越好(现实生活中也有很多抽象的例子,比如微波炉、汽车等都是简单的抽象),4)最好的模块是那些提供强大功能但又拥有简单接口的深度模块,模块的成本是其接口,接口越小且越简单,引入的复杂性就越少

🚩 注意「浅层模块」。拆大量的类和「任何超过 N 行的方法都应该被分解成多个方法」这种思路会导致大量的浅层类和方法,增加了整个系统的复杂性。

9、信息隐藏。实现深层模块的最重要技术是信息隐藏。在设计一个新模块时,你应该仔细考虑哪些信息可以隐藏在该模块中。如果你能隐藏更多信息,你也应该能够简化模块的接口,这会使得模块更加深入。

🚩 注意「信息泄露」。如果新类通过其接口暴露了大部分知识,那么它不会提供太多价值。

🚩 注意「时间分解」。时间分解中,系统的结构对应于操作发生的时间顺序。考虑一个应用程序,它读取特定格式的文件,修改文件内容,然后再次写出文件。采用时间分解,这个应用程序可能会被分解成三个类:一个用来读取文件,另一个用来执行修改,第三个用来写出新版本。结果是,时间分解往往导致信息泄露。在设计模块时,关注执行每个任务所需的知识,而不是任务发生的顺序。

🚩 注意「过度曝光」。如果一个常用功能的 API 强迫用户去了解那些很少使用的其他功能,这会增加那些不需要这些很少用功能的用户的认知负担。(比如默认值设置不当会导致这个问题,比如发 http 请求时每次都要设置协议版本,或者比如配置 webpack 时每次都要配置拆包策略)。

10、通用模块往往更深入。在设计新模块时,通常会面临一个选择,是用通用方式实现,还是用特殊方式实现。在作者的经验里,最佳实践是以某种通用方式实现新模块

内容预览已结束

此内容需要会员权限。请先登录以查看完整内容。