本章介绍了一种关于如何构建“更深层(deeper)类”的思考方式。
假设正在开发一个新的模块,并发现其中存在某些无法避免的复杂性。那么,更好的做法是什么?是让模块的使用者去处理这些复杂性,还是由模块在内部自行消化这些复杂性?如果这些复杂性与模块所提供的功能密切相关,那么通常第二种做法才是正确的选择。
大多数模块的用户数量都远远多于模块开发者,因此,与其让大量用户承受复杂性,不如让少数开发者承受复杂性。作为模块开发者,你应该努力让模块的使用者尽可能轻松,即使这意味着你自己需要付出更多工作。
换一种说法: 对于一个模块而言,拥有简单的接口,比拥有简单的实现更重要。
作为开发者,人们往往容易采取相反的做法:把简单的问题自己解决,把困难的问题丢给别人。
例如:
这些做法在短期内确实能让开发者轻松一些,但它们会放大系统复杂度,使得很多人都必须面对同一个问题,而不是仅由一个人解决。
例如:
考虑一个用于 GUI 文本编辑器的类,它负责管理文件中的文本内容。这个类已经在第 6 章和第 7 章中讨论过。
它提供以下功能:
当学生们实现这个类时,很多人选择了面向行(line-oriented)的接口:
提供以下方法:
这种设计使 Text 类本身的实现非常简单,但却给上层软件带来了复杂性。因为在用户界面层面,操作很少以“整行”为单位。
例如:
因此,为了实现用户界面功能,上层软件不得不频繁地:
而第 6.3 节介绍的那种 面向字符(character-oriented)的接口 则把复杂性向下拉取了。这样一来:
而无需自己拆分或合并文本行。因此,上层软件变得更简单。
当然,Text 类自身的实现会变得更复杂。如果内部仍然以“行集合”的形式存储文本,那么为了支持字符级操作,它必须自行完成:
等工作。
但这种做法更好,因为:它把“拆分与合并”的复杂性封装在 Text 类内部,从而降低了整个系统的总复杂度。
##示例:配置参数
配置参数是一个典型的例子:它们通常是在把复杂性向上传递,而不是向下吸收。
本来,一个类完全可以在内部决定自己的行为。但有时开发者会暴露一些配置参数,例如:
于是,用户必须自己决定这些参数应该设置成什么值。
如今配置参数已经非常流行。有些系统甚至拥有数百个配置项。
支持者认为,配置参数能够让用户根据自己的需求和工作负载来调整系统。在某些场景下确实如此,低层基础设施代码未必了解最佳策略,而用户可能更了解自己的业务领域。
例如,某些请求比其他请求更加关键,因此用户希望为这些请求配置更高优先级。在这种情况下,配置参数确实可能帮助系统适应更多场景,并获得更好的性能表现。然而,配置参数也给开发者提供了一种逃避问题的捷径:遇到困难,不解决,而是转交给别人。
很多情况下:用户或管理员其实很难,甚至根本不可能,确定这些参数应该设置成什么值。
而另一些情况下:系统完全可以稍微多做一点工作,在内部自动推导出合适的参数值。
例如,考虑一个需要处理丢包问题的网络协议。它发送请求后,如果在规定时间内没有收到响应,就会重新发送请求。为了决定重试间隔(retry interval),一种做法是增加配置参数,让用户自行设定。但实际上,协议完全可以自动计算这个值。
方法是:
这种做法把复杂性向下拉取到了协议内部。用户不再需要思考:“重试时间到底应该设多少?”除此之外,它还有额外好处:重试时间能够动态调整。当运行环境发生变化时:系统会自动适应。相比之下,配置参数往往会随着环境变化而逐渐失效。
因此:
应当尽可能避免配置参数。 在暴露一个配置参数之前,先问自己:
“用户(或者更高层模块)真的能比我们更准确地确定这个值吗?”
如果必须提供配置参数,那么应该尽量提供合理默认值(reasonable defaults)。这样用户只有在特殊情况下才需要修改配置。理想情况下:每个模块都应该彻底解决它负责的问题。而配置参数本质上意味着:这个问题并没有被完全解决。它只是把一部分复杂性转嫁给了别人,从而增加了系统复杂度。
在向下拉取复杂性时,需要保持克制。因为这个思想很容易被滥用。如果走向极端,那么你可能会试图把整个应用程序的所有功能都塞进一个类中。显然这是荒谬的。
只有满足以下条件时,把复杂性向下拉取才是合理的:
需要始终记住: 目标是降低整个系统的复杂度,而不是单纯地让某个类承担更多责任。
第 6 章曾提到,一些学生在 Text 类中定义了直接反映用户界面行为的方法。例如,专门实现 Backspace 键行为的方法。乍看之下,这似乎是在向下拉取复杂性。但实际上并不是。因为,把用户界面的知识塞进 Text 类中,并没有显著简化上层代码。同时,这些用户界面相关知识,与 Text 类的核心职责毫无关系。
因此在这个例子里:所谓的“向下拉取复杂性”实际上只是造成了信息泄漏(information leakage)。
开发模块时,应当主动寻找机会 让自己多承担一点痛苦,从而减少用户的痛苦。
换句话说:
开发者承担复杂性,
用户获得简单性。
这正是“向下拉取复杂度(Pull Complexity Downwards)”的核心思想,也是构建深层模块(Deep Module)的关键原则之一。