11子程序设计中的内聚性
一. 子程序设计
对于子程序而言,内聚性是指子程序中各种操作之间联系的紧密程度。有些程序员更喜欢使用“强度”这一术语:一个子程序中各种操作之间的联系有多强?想Consine()(余弦函数)这样的函数就是极端内聚的,因为整个程序只完成一项功能。而CosineAndTan() (余弦余正切)这个函数的内聚性相对较弱,因为它完成了多余一项的操作。我们的目标是让每一个子程序只把一件事情做好,不再做其他任何事情。
这样做得好处是得到更高的可靠性。一项针对450个子程序所做的研究发现,高内聚性的子程中有50%没有任何错误,而低内聚性的子程序中只有18%是没有错误的。
关于内聚性的讨论一般会涉及到内聚性的几个层次。理解一些概念要比记住一些特定的术语更重要。
1. 功能的内聚性
功能的内聚性是最强也是最好的一种内聚性,也就是让一个子程序仅执行一项操作。例如 sin() 这样的子程序都是高度内聚的。当然,以这种方式来评估内聚性,前提是子程序所执行的操作与其名字相符——如果它还做了其他的操作,那么它就不够内聚,同时其命名也有问题。
2. 不够理想的内聚性
2.1. 顺序上的内聚性
顺序上的内聚性是指在子程序内包含有需要特定顺序执行的操作,这些步骤需要共享数据,而且只有在全部执行完毕后才完成了一项完整的功能。
举一个顺序上的内聚性的例子,假设某个子程序需要按照给定出生日期来计算出员工的年龄和退休时间。如果子程序先计算员工的年龄,再根据他的年龄来计算退休的时间,那么他就有顺序上的内聚性。而如果子程序先计算员工的年龄,然后再重新计算他的退休时间,两次计算之间只是碰巧使用了相同的出生日期,那么这个子程序就只具有通信上的内聚性。
那么该怎样设计具有功能上的内聚性的子程序呢?你可以创建两个不同的子程序,他们能根据给定的生日分别计算员工的年龄和退休时间。其中,计算退休时间的子程序可以调用计算年龄的子程序。这样两者就都具有功能上的内聚性了。而其他的子程序则可以调用二者之一或者全部。
2.2 通信上的内聚性
通信上的内聚性是指一个子程序中的不同操作使用了同样的数据,但不存在其他任何联系。例如某个子程序先根据传给它的汇总数据打印一份汇总报表,然后再把这些汇总数据重新初始化,那么这个子程序就具有通信上的内聚性;因为这两项操作只是使用了相同的数据才彼此产生联系。
要改善这个子程序的内聚性,应该让重新初始化汇总数据的操作尽可能靠近创建数据汇总的地方,而不是放在打印报表的子程序里。应该把这些子程序进一步拆分成你个独立的子程序:一个负责打印报表,一个负责在靠近创建或修改数据的代码的地方重新初始化数据。然后在原本调用那个具有通信内聚性的子程序的更高层的子程序中调用这两个子程序。
2.3. 临时的内聚性
临时的内聚性是指含有一些因为需要同时执行才放到一起的操作的子程序。典型的例子有:Startup()、Shuntdown() 等。有些程序员认为临时的内聚性是不可取的,因为他们有时与不良的编程实践相关——比如在 Startup() 子程序里塞进一大堆互不相关的代码等。
为避免这个问题,可以把临时性的子程序看做是一系类实践的组织者。前面提到的 Sartup() 子程序可能需要读取配置文件、初始化临时文件、设置内存管理,再显示启动画面。要想使它最有效,应该让原来那个具有临时内聚性的子程序去调用其他的子程序,由这些子程序来完成特定的操作,而不是由它直接执行所有的操作。
这个例子提出这样一个问题,即如何选择一个能够在恰当的抽象层次上描述子程序的名字。你可能决定把一个子程序命名为 ReadConfigFileInitScratchFileEtc(),它可以暗示该子程序只有巧合的内聚性。而如果你把它命名为 Startup(),那么很明显,这个子程序就只具有一个功能,切具有功能上的内聚性。
3. 不可取的内聚性
一般来说,其他类型的内聚性都是不可取的。它们都会导致代码组织混乱、难于调试、不便修改。如果一个子程序具有不良的内聚性,那最好还是花功夫重新编写,使其具有更好的内聚性,而不是再去花精力精确地诊断问题所在了。因此,知道应该避免什么是非常有用的,下面列举一些不可取的内聚性。
3.1 过程上的内聚性
过程上的内聚性是指一个子程序中的操作是按特定的顺序进行的。一个例子是依次获取员工的姓名、住址和电话号码的子程序。这些操作执行的顺序之所以重要,只是因为它和用户按屏幕提示而输入数据的顺序相一致。另一个子程序用来获取员工的其他数据。这段程序也具有过程上的内聚性,因为它把一组操作赋以特定的顺序,而这些操作并不需要为ile除此之外的任何原因而彼此联系。
为了得到更好的内聚性,可以把不同的操作纳入各自的子程序中。让调用放你的子程序具有单一而完整的功能:GetEmployee() 就比 GetFirstPartOfEmployeeData() 更为可取。你可能还需要修改用来读取其余数据的子程序。为了让所有的子程序都具有功能上的内聚性,对两个或更多的原有子程序进行修改是很常见的。
3.2 逻辑上的内聚性
逻辑上的内聚性是指若干操作被放入同一个子程序中,通过传入的控制标志选择执行其中的一项操作。之所以称之为逻辑上的内聚性,是因为子程序的控制流或所谓的“逻辑”是将这些操作放到一起的唯一原因——他们都被包在一个很大的 if 语句或者 case 语句中,而不是因为各项操作之间有任何逻辑关系。
如果子程序里的代码仅由一系列的 if 语句或者 case 语句,以及调用其他子程序的语句组成,那么创建这样一个具有逻辑上的内聚性的子程序通常也是可以的。在这种情况下,如果子程序唯一的功能就是发布各种命令,其自身并不做任何处理,着通常一个是一个不错的设计。这类子程序的技术术语便是“事件处理器”。事件处理器通常用在各种交互性的环境中,例如像 Apple Macintonsh、Microsoft Windows 以及其他一些 GUI 环境。
3.3. 巧合的内聚性
巧合的内聚性是指子程序中的各个操作之间没有任何可以看到的关联。它也可称为”无内聚性“或”混乱的内聚性“。