前言

最近阅读了一本比较短小精悍的软件设计方面的书《A.Philosophy.of.Software.Design》,这里把书中的主要内容总结了一下,主要是结合我多年来对软件设计的一些理解,我挑了其中觉得非常重要的一部分内容成文。

设计原则

软件设计的最主要任务就是降低系统复杂性,软件开发活动中任何导致软件复杂性变化的活动都需要被及时识别和处理。

但是现实世界永远不可能是完美的,消除所有复杂性的代价非常高,如果系统中某些部分固有的复杂性无法被消除(比如遗留系统或者复杂的业务逻辑),那么可以考虑把这些复杂性隐藏起来,或者只让少部分人接触这些复杂性。

软件复杂性的三个主要表现:修改放大(Change amplification), 认识负担(Cognitive load) 和隐藏的未知(Unknown unknowns).

当你发现往系统中添加一个功能的时候,你需要修改系统中的很多模块和代码文件,那么此时系统的复杂性是非常高的,它不符合开闭原则。

如果实现一个功能需要阅读大量的说明文档和已有代码,并且经常需要下断点调试程序才能搞懂代码的意思,那么说明此系统的认知复杂性非常高,人们无法一眼就知道代码干了些什么事,这是个非常危险的信号。

如果系统中的所有人都害怕修改代码,担心微小的改动可能会导致软件出 bug, 或者任何一些稍微复杂的改动都会导致系统出一些莫名其妙的 bug, 可能的原因就是系统里面埋了太多 Unkown 了。

复杂性是一点点累积的,这一点通常会被很多人所忽略。软件复杂性的累积通常源于开发团队的代码坏味道的无下限的容忍,没有 Code Review, 没有对设计做迭代。基层管理者需要每个版本迭代访问做需求的人,是否在实现功能的时候做了一些“很聪明的 Hack", 这些 Hack 可能会导致软件系统的复杂性急剧飙升。因为它们通常会以各种 Special case 的方式来糊上需求,导致代码难以理解。

降低系统复杂性的方法

降低复杂性主要靠抽象和封装,减少依赖和晦涩的代码,尽可能把复杂性消除,模块之间的依赖要尽量少并且只能依赖抽象和接口,而不能依赖具体的实现。另外,对于系统设计中的想法,一定要有文档记录下来,最好是设计在迭代过程的重要的想法也一并记录下来,形成版本记录。这样后来的人在阅读代码和设计的时候就知道 Why 了。

软件设计是一项平衡的艺术,任何理论、原则和方法如果过度盲从都有可能导致伤害。所以,我们需要好队友,需要招聘 great coders 而不是平庸的 coders 。 因为好的代码和设计是日积月累的,平庸的 coder 一般只是满足于功能的实现,不注重编写清晰的代码,也不会管自己的代码是否给他人带来了更多的问题。

人的大脑认知容量有是限的,我们要善于利用信息隐藏、模块化、接口和文档来减少认知负担。如果某一个类、方法或者属性无法从名字就判断它们的用途,那么它们就会带来认知负担。所以命名是非常重要的,最好是给系统中常用的命名提供一套“术语词典”,这样可以让团队里面前后台同学都采纳一致的命名规则。

某些情况下,文档和注释也是绝佳的降低系统复杂性的工具。一味追求代码自解释,有可能会非常困难并且不切实际。但是注释不能简单地重复代码的功能,它应该要增加一些设计意图和隐藏在代码之外的细节信息,这些细节对于维护者非常关键。注释要着重关注做了什么,而不是怎么做。

实用主义的想法

下面这些内容是我的一些个人看法,当然部分也来自书籍,主要是为了给今后的自己提供一些实际操作的指引。

设计

  1. 设计两次,第一次设计完成业务需求,第二次设计更好地为今后扩展预留接口。
  2. 不同的模块设计目标是不一致的,有的注重扩展性,有的注重性能。
  3. 可以考虑注释驱动设计。在扩展系统的时候,可以先编写关键代码的注释,看看能否通过这些注释了解整个新增模块的运行流程。
  4. 设计是迭代的,不可能一次做对。另外软件系统也是在不断变化的,设计必须跟着一起进化。商业项目很难在每次实现功能之前都进行重构,时间上不允许。这种可以考虑事后重构,也可以考虑在迭代中间留 4 天时间来做重构(这 4 天可以做一些需求评审、测试用例评审、计划安排的事情)

Code Review

  1. Code Review 需要 Review 的是设计和修改的代码给系统带来的复杂性。
  2. 术语、命名、风格等信息尽可能依赖工具来解决
  3. 一致性非常重要,如果是遗留系统,尽可能保持跟之间的风格一致比更正确的风格更好。
  4. 尽量把一些常规的业务开发模块化和工具化,采用代码生成工具来辅助编写代码会让代码结构更符合设计。

文档

  1. 文档需要记录设计迭代过程中的 idea
  2. 代码的注释要记录代码中隐藏的信息或者当时实现所做的妥协,需要定期安排人力去清理这些妥协
  3. 如果简单的文档不能把一个模块所做的事情说清楚,那么说明这个模块的功能太复杂了。
  4. 文档一定要事前写,而不是等到代码写完了再去补充。

工具

  1. 工具可以标准化流程、自动化任务,同时还能降低人为引入的错误
  2. 尽可能编写工具把复杂的编码活动简单化,利用可视化编程工具来降低系统复杂性
  3. 多编写能够自动生成代码的工具,这些工具对降低系统复杂性非常有帮助。
  4. 游戏开发团队中的各类人员永远都需要更好用的工具和更快的工具

结束

软件设计原则会随着业务领域的不同而侧重点有一些差异,比如有些系统更加注重性能,而另一些系统则更注重正确性。设计好的系统是需要付出成本的,所以我们需要识别系统中最重要的模块是什么,然后对这部分功能进行设计迭代。

如果一个模块频繁被修改,那么此时就应该花费更多的时间来进行设计。如果一个模块从来没人动它,那么应该想办法隐藏这个模块的复杂性。

有时候可以考虑让开发速度稍微慢一点,bug 数量少一点。敏捷开发绝不只是快速开发,还应该包括迭代设计。