工作三年以来一直对写出设计优雅且可读性较好的代码抱有执念,最初接触到的关于代码整洁和软件设计的书是《代码整洁之道》,这本书大概在我入职半年时读完,并在很长的一段时间内将其中谈到的“每个方法只做一件事”、“方法长度最多不要超过 5 行”和“优秀的代码都是自解释的,很少会有注释”等等观点奉为圭臬,但是由于其成书较早,其中的一些观点显然已经不再使用当前业务开发环境了。就拿前两点来说,看上去能让每个小方法尽可能的简单,但是对于复杂的业务系统来说,这将会产生很多的小方法堆叠,产生“方法风暴”,在看一个方法时,需要不断的在各个小方法中往复跳转,使得可读性大大降低。


然而,《软件设计哲学 A Philosophy of Software Design》我觉得相对来说是更符合当前软件开发的指导书,其中谈到了与其相反的观点:“方法的可读性并不取决于它的长度”,“隔离复杂性,设计深的模块”和“写完善的注释内容”等等,我认为这些和我们当前的开发是相契合的,以其中的原则作为开发设计指南也是非常合适的。


本文则主要是对其中谈到的部分观点进行讨论,深入学习还是推荐大家去看原书。

设计“深”的类

设计较好的类通常功能强大但是公开出的接口非常简单,这样的类被称为“深”的。什么是“深”的类呢?如下图所示:

《软件设计哲学》:新“代码整洁之道”_软件设计


如果用矩形来表示类的话,则矩形的面积与类提供的功能成正比;矩形顶部边缘表示类公开出的接口,边缘长度越长则表示接口越复杂。较“深”的类,因为其内部复杂性只有很小一部分对调用者可见,所以称它设计较好。


以 Java 语言中的垃圾回收器为例,它在后台操作垃圾回收,实现非常复杂,而这种复杂性对程序员却是隐藏的,因为它根本没有公开出任何接口。


不过,在《代码整洁之道》中,深类的价值并没有得到肯定,而且在软件设计的传统观点中也认为:“类应该小,而不是深”,一些较大的类通常会被鼓励拆分成多个小类。这种设计原则会导致创建大量的浅类(每个类都很简单),在《软件设计哲学》中被称为“多类症”,是一种冗长的编码风格。这些浅类都具有自己的接口,但并不会贡献太多的功能,随着这些浅类的累积,系统的复杂性会随之增加。


Java IO 类库是“多类症”很好的例子,比如我们要在文件中获取序列化的对象,要写如下代码:


FileInputStream fileStream = new FileInputStream(fileName);


BufferedInputStream bufferedStream = new BufferedInputStream(fileStream);


ObjectInputStream objectStream = new ObjectInputStream(bufferedStream);
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.

复制代码


我们必须创建 3 个对象来完成这个操作,前两个步骤中 FileInputStream 提供基本的 I/O,BufferedInputStream 添加执行缓冲的功能,而实际上我们想要的只是最后创建的 ObjectInputStream 对象,这使编码看上去比较冗余。尽管这使得各个类的职责更加分明,并且允许用户创建不带有缓存机制的序列化对象,但这并没有带来简单易用的结果。提供选择固然是好的,但如果没有因此使其简单易用则背离了设计的初衷。

好好写注释

花时间来好好为变量和方法命名,是非常值得的,它能大大的提高可读性,最好的情况是:当读者看到它时,就已经基本领会了它的作用。尽可能的让它们明确、直观且不太长。如果很难为变量或方法找到一个简单的名称,那么这可能暗示底层对象的设计不够简洁,可以考虑拆分成多个分别定义或者为其添加上必要的注释。


但《代码整洁之道》中却对注释持有消极的观点:


... 注释最多只能算是一种不得已而为之的手段。若编程语言有足够的表达力,或者我们长于用这些语言来表达意图,就不那么需要注释——也许根本不需要。

注释的恰当用法是弥补我们在代码中未能表达清楚的内容... 注释总是代表着失败,我们总有不用注释便很难表达代码意图的时候,所以总要有注释,这并不值得庆贺。


曾经我也对这个观点信以为然,但是随着我在开发中尽力写自解释的代码时却发现:紧靠几个简短的词语并不能将方法的作用解释清楚,想让它自解释就会导致方法名写的很长,而且多数情况下,研发同事并不愿意花精力去翻译那冗长又蹩脚的方法名,给人更多的感受是:“这写的都是什么?”


后来,我渐渐放弃了写自解释的代码,并通过添加注释来增加可读性,这不仅仅减少了大家对代码提出的抱怨,而且还减轻了为方法命名的压力。 “好的代码是自解释的”的观点也在心中祛魅,它其实更像是程序员心中的美好幻想


注释除了能提高可读性之外,还能隐藏复杂性,提高抽象程度。它能对接口实现进行概括,相当于是实现的简化视图,如果读者必须要去接口实现中研究每一段代码才能了解它的功能的话,那么就谈不上任何抽象了。


还有一点值得注意的是:注释并不是弥补方法名表达能力欠佳的补丁,也不是简单的对代码的重复,而是代码书写前的先决条件。因为先写注释能带来很多好处:


  • 能够将设计时想到的东西记录下来,而且方便后续维护
  • 先写注释能更在开发进行时衡量设计,如果方法或变量的注释能够以非常简洁的描述来概括它的功能,那么通常设计是较好的,如果注释非常冗长,而且其中暴露出很多具体实现相关的内容,那么设计可能需要重新考虑
  • 如果你崇尚良好设计的话,那么写注释是一件有意思的事情,因为你会发现,你的设计是如此的简洁,以至于你只需要写一两句话就能描述清楚它的功能
  • 如果使用 AI 代码自动补全功能的话,你将能更强烈地感受到它的威力


随着注释写的越来越多,你会发现:注释其实是代码的一部分,因为它不光提供代码之外的重要信息,还反映了开发者对代码的设计和重视,随着时间的推移,有新的开发者加入时,也能让他快速理解代码,降低出现 Bug 的概率。

注意事项

《软件设计哲学》中还提到了一些在开发中需要谨慎注意的事情:

尽可能少用配置参数

这个观点让我想到在开发补购查询接口时为其添加的配置,其中可对查询的数据规模和相关品类信息等进行配置。虽然该配置仅供研发使用,但是实际上这提高了接口的复杂性。为了将其带来的复杂性降到最低,采用了以下两种方法:


  • 详细的注释:为配置类中的每个字段添加上详细的注释,让研发能清晰的了解每个参数的作用
  • 指定默认值:当某些参数不被配置时,提供默认值来满足基本的功能


自适应变更配置是书中提到的一个不错的观点,通过代码的执行情况不断自适应调整参数值,减少人为的干预,但是我认为这种方法应用的场景比较有限,它更像是算法中参数的自适应调整。

保持一致性

有些项目可能开发较早,并没有随着软件设计的发展而实时调整,但这并不能算得上是什么坏事,只要大家在项目中遵循现有约定,也能维持较好的系统设计。反倒说贸然地引入新的设计原则,造成新原则和旧原则并存才是更糟糕的,因为这很可能会导致混乱。

寻求通用的设计

在开发需求时,尽可能不进行定制化开发,而是思考通用的实现逻辑,即使在不考虑复用的情况下,通用性代码也是更合理的。在进行开发设计时,可以尝试思考如下问题来引导自己找出通用设计:


  • 满足当前需求最简单的接口是什么?
  • 这个方法会在多少种情况下被使用?
  • 目前通用的 API 使用起来是否简单?


不管是专用的类或方法还是代码里的特殊情况,都是软件复杂性的主要来源。专用代码无法完全消除,但通过良好的设计能够显著减少专用代码,并将专用代码与通用代码分开。这能使类更深、隐藏复杂性以及让代码更简单、更清晰。

终:没有孰是孰非

我觉得《软件设计哲学》是站在代码阅读者的角度上,考虑的是该如何设计能让读者更轻松的读懂,降低复杂性,像其中的提到的“永远不要反驳他人对代码可读性的评价”的观点,都是在对其进行强调。而《代码整洁之道》给我的感受是站在代码的书写者身上,因为我觉得像“一个方法只做一件事”、“代码长度不要超过多少行”和“尽可能不写注释”等原则,更多的是强调如何写,或者说是让研发人员关注如何写上,对于如何让读者读懂,是没有深入去考虑的。当然《代码整洁之道》也并不是一无是处,比如其中谈到:“方法的排列要自上而下,让读者看起来就像读报纸一样,并不是简单的将公有或私有方法排列在一起”,这对代码的可读性也是有帮助的。所以,这两本书更适合结合起来比较阅读。


更重要的是,《软件设计哲学》强调要成为一名软件设计师,并不断提高设计技能,降低系统的复杂性。虽然这可能会将大部分时间花在设计上,但是这些设计会很快带来回报,尤其是需要对这部分功能一遍又一遍地迭代时,你一定会发出“幸亏做了良好设计”的感叹,并从中获得源源不断的快乐。