Philosophy of Software Design - 第八章 降低复杂性

这章介绍了另外一种如何设计更有深度的类的考虑方式。假设你正在新建一个模块,然后发现了一处无法避免的复杂实现。下面哪种处理方式更好:应该让模块的使用者处理这个复杂实现吗?还是应该由你在模块内部处理。如果这个复杂实现和模块提供的功能有关,那么通常第二个答案是对的。大多数模块的使用者要多于开发者,所以由开发者受苦要好于使用者受苦。作为模块开发者,你应当尽全力方便使用者,即使那意味着额外的工作量。另外一种表述这个观点的方式是,模块有简单的接口比简单的实现更重要。

作为一个开发者,先实现简单功能,把复杂部分留给其他人的想法非常有诱惑力。如果出现了你不知道该如何处理的状况,最简单的办法就是抛出异常让调用者去处理。如果你不确定实现哪种策略,可以定义一些调整策略的配置参数,并留给系统管理员去找出最好的参数。

像这样的解决办法会让你的生活在短期内更加轻松,但是它们放大了复杂度:许多人都必须处理同一个问题,而不是让同一个人去处理。比如,如果一个类抛出了一个异常,这个类都每个调用者都必须处理它。如果一个类暴露了配置参数,每个系统管理员在每次安装部署时,都需要学习一遍如何配置它们。

8.1 例:文本编辑器类

考虑一下我们第 6、7 章讨论过的文本编辑器类,它为图形化界面的文本编辑器提供管理文本文件的功能。这个类提供了将文件从硬盘读取到内存、查询和修改文件在内存中的副本、将修改后的版本写回硬盘的功能。当学生们要实现这个类时,大多数都会选择面向文本行的接口,并提供读取、插入和删除整行文本的方法。这会得到一个实现简单、但是高层软件调用起来却很复杂的类。在用户接口调用这个级别,操作很少涉及到一整行。比如,按键操作导致单个字符插入到已存在的行中;拷贝或删除选择区域可以修改部分或多行。使用面向文本行的接口时,高层软件在实现用户接口时就不得不对行做拆分合并。

像 6.3 章节中描述的面向字符的接口会将复杂性拉低。用户接口可以插入、删除任意长度的文本,而不需要拆分合并行:这样调用变简单了。文本类的实现可能会变复杂:如果内部使用行的形式来代表文本,那它就需要拆分合并行来实现面向字符的操作。这种实现方法更好,因为它把拆分合并的复杂度封装在了文本类中,这会降低系统总体的复杂度。

8.2 例:配置参数

配置参数是将复杂性上移而不是下移的例子。一个类可以通过暴露一些控制它行为的配置参数,来代替在内部决定一个特定的行为,比如缓存大小或放弃前重试请求的次数。那么,用户就必须为参数指定一个合适的值。现在系统中的配置参数已经变得非常流行;一些系统有上百个参数。

支持者为配置参数的合理性争辩道,它们允许用户根据自己特定的需求和工作负载调整系统。在一些场景中,低层的基础代码很难知道要应用的最好策略,而用户对他们的领域要熟悉的多。比如,用户可能会知道一些请求要比其他请求对时间更敏感,所以让用户为这些请求设置更高的优先级是说得通的。在类似的场景中,配置参数可以在更多的不同领域中获得更好的性能。

然而,配置参数也为逃避处理重要事项并把它们转给其他人处理提供了借口。在许多场景中,用户或管理员很难甚至不可能为参数设置正确值。在另外的场景中,在系统实现时很少的额外工作就可以自动的设置正确的参数值。考虑一个需要处理丢包的网络协议。如果它发送了一个请求,在一定的时间间隔内却没有收到恢复,它就会重新发送请求。决定重试间隔的一种方式是引入一个配置参数。然而,传输协议可以通过自己测量请求成功的回复时间计算出一个合理的值,并使用它的倍数作为重试间隔。这种方式将复杂性拉低,节约了用户找出正确重试时间的力气。它还带来了可以动态计算重试间隔的好处,所以在条件变化时可以自动调整。相反,配置参数很容易过时。

因此,应当尽量避免使用配置参数。在暴露一个配置参数前,问一下自己:“相较于我们自己决定参数值,调用者(或高层模块)可以知道更好的配置吗?”当你创建了配置参数时,看依稀你是否可以自动计算出合理的默认值,这样用户只需要在个别条件下提供配置参数值。理想情况下,每个模块应当完整地解决一个问题;配置参数会使解决方案变得不完整,这将增加系统的复杂度。

8.3 过度实践

当要把复杂性拉低时需要谨慎考虑;这是一个容易过度实践的主意。一个极端的方式会是,将一整个应用的所有的功能都在单个类中实现,这明显是说不通的。拉低复杂性只在以下情况中说得通:(a)复杂性和类中存在的函数有密切关系时 (b) 拉低复杂性可以简化应用中其他许多地方的实现(c)拉低复杂性简化了类的接口。要记住真正的目标是最小化整个系统的复杂度。

第六章描述了一些学生是如何在文本类中定义了会反映用户接口的方法,比如实现了回退健功能的方法。看上去这个实现好像是没问题的,因为它把复杂性拉低了。然而,将用户接口所需了解的知识强加到文本类中并不会大幅简化高层代码,而且用户接口相关的知识和文本类的核心功能关系也不大。在这种情况中,拉低复杂性只导致了信息泄漏。

8.4 结论

当开发一个模块时,应当尽量将调用者的痛苦转移到自己身上。