Philosophy of Software Design 第十一章 设计两次

设计软件是很难的,所以当你考虑如何构建一个模块或系统时,不太可能第一个想法就会产生最好的设计。如果对每个主要的设计决定都多考虑几个选项,会得到一个好得多的结果:设计两次

假设你正在设计图形化文本编辑器中管理文本的类。第一步是定义这个类由编辑器中其他部分使用的的接口;与其使用第一个蹦到脑子里的主意,不如多考虑几种可能性。其中一个选择是面向行的接口,插入、修改和删除整行的文本。另外一个选择是基于单个字符的插入和删除接口。第三个选择是面向字符串的接口,对可能跨越行边界的任意范围的字符进行操作。不需要把每个选项的所有功能特性都确定下来;到目前为止,把几个重要的方法大概轮廓勾勒出来就足够了。

尝试选择和其他几种有本质区别的方式;这样你可以学到更多。即使你很确定只有一种合理的方式,也要考虑一下第二种设计方式,不管你认为它会有多糟糕。思考那个设计的弱点并且把它和其他设计的功能做对比是很有启发性的。

当你粗略地描绘出几种不同的设计后,列一个每个设计的优缺点的表格。最重要的考量是,这个接口对高层的软件来说是否易用。在上面的例子中,面向行和面向字符的接口都需要使用文本类的接口做额外的工作。使用面向行的接口时,当遇到部分或跨行的操作比如剪切和复制选区时,高层的软件需要拆分和合并行。使用面向字符的接口时,对于需要修改多个字符的操作需要循环实现。另外也需要考虑如下几种因素:

  • 其中一种选择比其他有着更简单的接口吗?在文本类的例子中,所有的文本接口都差不多简单。
  • 其中一个接口比其他更通用吗?
  • 使用其中一个接口的实现会比其他更高效吗?在文本类的例子中,面向字符的接口很可能会比起另外的慢很多,因为它要求每一个字符都调用文本类一次

当你比较过几种不同的设计以后,你将可以更好地确定最佳设计。最好的选择可能是可选项中的一种,或者你可能发现可以把多个选项的功能组合成一个新的设计,会比原来的选择更好。

有时每个选择都不太好;当这种情况发生时,考虑一下能不能想到另外的方案。用你在原有选择中发现的问题来驱动新的设计。如果你正在设计文本类并且只想到了面向行和面向字符的这两种设计方式,你可能会注意到,这两种方式用起来都很别扭,因为它们要求高层软件做额外的文本操作。这是一个红色警告:如果需要有一个文本类,那么它应该处理所有和文本相关的操作。为了消除额外的文本操作,文本接口应该和高层软件中的操作更匹配。这些操作并不总是对应到单个字符或单行。这条推理应该引导你得到一个面向范围的 API,这会消除之前设计中的问题。

设计两次的原则可以应用在系统中的许多层级。对于模块来说,你可以先用这个原则来选择接口,像上面描述的那样。然后当你设计实现时可以再次应用这个原则:对于文本类来说,你可能考虑过各种实现比如行的链表,固定长度的字符块,或者“gap buffer”。设计实现时的目标和设计接口时并不一样:对于实现来说,最重要的是简洁和性能。在设计系统的高层时多探索几种可能性也是很有用的,比如当选择用户界面的功能时,或者将系统拆解为主要模块时。在每个例子中,如果能够比较几个不同的选择就可以更容易发现最好的方案。

设计两遍并不需要占用非常多额外的时间。对于一个小模块比如类来说,你可能都用不到一两个小时来考虑可选项。相比于在实现类时你需要花去好几天甚至好几个星期,这是非常少的时间了。最开始的设计实验很可能最终会有助于得到一个更好的设计,收益会远大于在设计两遍时花费的时间。对于更大的模块,最开始的设计探索会花费更多的时间,但是实现也会更慢,而且更好的设计的收益也会更高。

我注意到设计两遍的原则有时候对非常聪明的人来说很难接受。当他们成长时,聪明人发现他们关于任何问题的第一个快速的想法都足够好;没有必要再考虑第二种或者第三种可能。这就培养了一种糟糕的工作习惯。然而,当这些人更年长一些,他们不断提升进入有着越来越难的问题的环境中。最终,每个人都会到达第一个想法不够好的地步;如果你想要获得真正厉害的结果,就必须考虑第二种可能,或者可能是第三种,不管你有多聪明。大型软件系统的设计就属于这个分类:没有人可以第一次尝试就把它做对。

不幸的是,我经常看到坚持实现第一个想法的聪明人,这导致他们无法发挥真正的潜能(同时也使得和他们共事令人沮丧)。或许他们下意识地认为“聪明人可以第一次就做对”,所以如果他们尝试不同地设计就会表明他们其实并不聪明。不是这样的。不是你不聪明;而是问题真的很难!而且,这是一件好事:相比完全不用动脑的简单问题,解决需要仔细思考的难题要有趣得多。

设计两遍的方式不仅提高了你的设计,而且可以提高你设计的技能。设计和比较不同方式的过程会教给你影响设计好坏的因素。随着时间的推移,排除糟糕的设计并磨练真正厉害的设计对你来说会越来越容易。