第 11 章 设计两次
设计软件非常困难,因此关于如何构造模块或系统,最初的想法不太可能就是最佳设计。如果在每个主要设计决策上都考虑多个方案,最终就能获得好得多的结果:设计两次。
本章概览最先想到的设计,为什么不该直接采用?
假设你正在为图形界面文本编辑器设计一个类,用来管理文件中的文本。第一步是定义这个类向编辑器其余部分提供的接口。与其采用最先想到的方案,不如考虑几种可能性。一种选择是面向行的接口,支持插入、修改和删除整行文本。另一种选择是以单个字符为单位进行插入和删除的接口。第三种选择是面向字符串的接口,可以操作任意范围的字符,包括跨行的范围。你无需确定每个备选方案的所有功能;在这个阶段,勾勒出几个最重要的方法就足够了。
尝试选择彼此差异很大的方案,这样你会学到更多。即使你确定只有一种合理的方案,也要考虑第二种设计,不管你认为它有多糟糕。思考该设计的弱点,并与其他设计的特性进行对比,会很有启发。
粗略设计出备选方案后,列出各自的优缺点。设计接口时,最重要的考虑因素是更高层级的软件是否易于使用它。在上面的示例中,面向行的接口和面向字符的接口都要求使用文本类的软件做额外的工作。使用面向行的接口时,更高层级的软件在操作一行中的部分文本或多行文本时(例如剪切和粘贴所选内容),需要自行拆分和合并行。使用面向字符的接口时,修改多个字符需要通过循环实现。还有其他值得考虑的因素:
- 某个方案的接口是否比其他方案更简单?在文本示例中,所有文本接口都相对简单。
- 一种接口是否比另一种接口更通用?
- 某个接口是否能支持比其他接口更高效的实现?在文本示例中,面向字符的方案可能比其他方案慢得多,因为它需要为每个字符单独调用文本模块。
比较备选设计之后,你就更容易确定最佳设计。最佳选择可能是其中一个方案,也可能是将多个方案的特性组合起来,形成比任何一个原方案都更好的新设计。
有时所有的备选设计都不够理想。这时,看看能否提出其他方案,并以已有方案中发现的问题为出发点,探索新设计。如果你在设计文本类时只考虑了面向行和面向字符的方案,可能会发现两者都不太好用,因为它们都要求更高层级的软件执行额外的文本操作。这是一个危险信号:既然有了文本类,它就应该处理所有文本操作。为了消除这些额外的文本操作,文本接口需要更贴合更高层级软件所执行的操作。这些操作并不总是对应单个字符或单行文本。沿着这个思路,你应该能找到面向范围的文本 API,消除先前设计中的问题。
译者图解用同一操作检验三个接口
设计两次的原则可以应用于系统的不同层级。对于模块,你可以先用这种方式选择接口,如上所述,再在设计实现时应用一次:对于文本类,可以考虑的实现包括由各行组成的链表、固定大小的字符块或“间隙缓冲区(Gap Buffer)”。实现与接口的设计目标不同:对于实现,最重要的是简单性和性能。在系统的更高层级上探索多种设计也很有用,例如为用户界面选择功能,或将系统分解为主要模块时。无论哪种情况,比较几个备选方案,都更容易找到最佳方案。
设计两次不需要花费很多额外的时间。对于类这样较小的模块,考虑备选设计可能只需一两个小时。相比实现该类所需的数天或数周,这点时间很少。最初的设计探索很可能带来明显更好的设计,其收益足以补偿设计两次所花的时间。对于较大的模块,初期的设计探索需要更多时间,但实现所需的时间也更长,更好的设计带来的收益也更大。
我注意到,十分聪明的人有时反而很难接受设计两次的原则。聪明的人在成长过程中会发现,面对任何问题,最先想到的方案就足以取得好成绩,无需考虑第二种或第三种可能性。这容易导致不良的工作习惯。然而,随着年龄增长、职位提升,他们面对的问题会越来越难。最终,每个人都会发现,最初的想法已不够好。如果想获得出色的结果,无论多么聪明,都必须考虑第二种可能性,甚至第三种。大型软件系统的设计就属于这种情况:没有人能只靠第一次尝试就做好。
不幸的是,我经常看到聪明的人坚持实现最先想到的方案,这使他们无法充分发挥潜力(也让与他们共事的人感到沮丧)。也许他们下意识地相信“聪明的人第一次就能做好”,所以尝试多种设计就意味着自己并不聪明。事实并非如此。不是你不聪明,而是问题真的很难!而且,这其实是件好事:解决一个需要认真思考的难题,比解决一个根本无需思考的简单问题有趣得多。
设计两次不仅能改善设计,还能提高你的设计能力。构思并比较多种方案,能让你了解哪些因素会让设计变好或变差。久而久之,你就能更容易地排除不好的设计,找到真正出色的设计。