首页 新闻 论坛 群组 Blog 文档 下载 读书 Tag 网摘 搜索 开源 FAQ 第二书店 博文视点 程序员
频道: 研发 数据库 中间件 信息化 视频 .NET Java 游戏 移动 服务: 人才 外包 培训
    图书品种:235680
       
热门搜索: ASP.NET Ajax Spring Hibernate Java


Design at the Routine Level

在子程序层上设计

内聚性(cohesion)的概念是由Wayne StevensGlenford MyersLarry Constantine1974年发表的一篇论文中提出来的。其他一些更为现代的概念,如抽象和封装等,通常在类这一层次的设计中更为适用(事实上,抽象和封装在类层次上已经在很大程度上取代了内聚性),但内聚性的概念仍然存在,而且在单个子程序这一层次上,仍是设计时常用的启发式方法。

对子程序而言,内聚性是指子程序中各种操作之间联系的紧密程度。有些程序员更喜欢使用“强度(strength)”这一术语:一个子程序中各种操作之间的联系有多强?像Cosine()(余弦函数)这样的函数就是极端内聚的,因为整个程序只完成一项功能。而CosineAndTan()(余弦与正切)这个函数的内聚性相对较弱,因为它完成了多于一项的操作。我们的目标是让每一个子程序只把一件事做好,不再做任何其他事情。

这样做的好处是得到更高的可靠性。一项针对450个子程序所做的研究发现,高内聚性的子程序中有50%没有任何错误,而低内聚性的子程序中只有18%是没有错误的(CardCarch and Agresti 1986)。另一项针对另外450个子程序(选取了同样数量的子程序进行研究纯属巧合)所做的研究发现,耦合度与内聚性之比最高的那些子程序【译注:也就是耦合度高且内聚性低的子程序】,其中所含的错误是耦合度与内聚性之比最低的子程序的7倍之多,而其修正成本则为20倍(Selby and Basili 1991)。

关于内聚性的讨论一般会涉及到内聚性的几个层次。理解一些概念要比记住一些特定的术语更重要。这些概念可以帮助你思考如何让子程序尽可能地内聚。

功能的内聚性functional cohesion)是最强也是最好的一种内聚性,也就是说。让一个子程序仅执行一项操作。例如sin()GetCustomerName()EraseFile()CalculateLoanPayment()以及AgeFromBirthdate()这样的子程序都是高度内聚的。当然,以这种方式来评估内聚性,前提是子程序所执行的操作与其名字相符——如果它还做了其他的操作,那么它就不够内聚,同时其命名也有问题。

除此之外,还有其他一些种类的内聚性却通常人们认为是不够理想的:

           顺序上的内聚性sequential cohesion)是指在子程序内包含有需要按特定顺序执行的操作,这些步骤需要共享数据,而且只有在全部执行完毕后才是完成了一项完整的功能。

举一个顺序上的内聚性的例子,假设某个子程序需要按照给定出生日期来计算出员工的年龄和退休时间。如果子程序先计算员工的年龄,再根据他的年龄来计算退休时间,那么它就具有顺序的内聚性。而如果子程序先计算员工的年龄,然后再重新计算他的退休时间,两次计算之间只是碰巧使用了相同的出生日期,那么这个子程序就只具有通信上的内聚性(communicational cohesion

那么该怎样设计具有功能上的内聚性的子程序呢?你可以创建两个不同的子程序,它们能根据给定的生日分别计算员工的年龄和退休时间。其中,计算退休时间的子程序可以调用计算年龄的子程序。这样两者就都具有功能上的内聚性了。而其他的子程序则可以调用二者之一或全部了。

           通信上的内聚性communicational cohesion)是指一个子程序中的不同操作使用了同样的数据,但不存在其他任何联系。例如某个子程序先根据传给它的汇总数据打印一份汇总报表,然后再把这些汇总数据重新初始化,那么这个子程序就具有通信上的内聚性:因为这两项操作只是因为使用了相同的数据才彼此产生联系。

要改善这个子程序的内聚性,应该让重新初始化汇总数据的操作尽可能靠近创建汇总数据的地方,而不是放在打印报表的子程序里。应该把这些子程序进一步拆分成几个独立的子程序:一个负责打印报表,一个负责在靠近创建或修改数据的代码的地方重新初始化数据。然后在原本调用那个具有通信内聚性的子程序的更高层的子程序中调用这两个子程序。

           临时的内聚性temporal cohesion)是指含有一些因为需要同时执行才放到一起的操作的子程序。典型的例子有:Startup()CompleteNewEmployee()Shutdown()等。有些程序员认为临时的内聚性是不可取的,因为它们有时与不良的编程实践相关——比如说在Startup()子程序里塞进一大堆互不相关的代码等。

为避免这个问题,可以把临时性的子程序看做是一系列事件的组织者。前面提到的Startup()子程序可能需要读取配置文件、初始化临时文件、设置内存管理器,再显示启动画面。要想使它最有效,应该让原来那个具有临时内聚性的子程序去调用其他的子程序,由这些子程序来完成特定的操作,而不是由它直接执行所有的操作。

这个例子提出这样一个问题,即如何选择一个能够在恰当的抽象层次上描述子程序的名字。你可能决定把一个子程序命名为Read­Config­File­Init­Scratch­File­Etc(),它可以暗示该子程序只有巧合的内聚性(coincidental cohesion)。而如果你把它命名为Startup(),那么很明显,这个子程序就只具有一个功能,且具有功能上的内聚性。

一般来说,其他类型的内聚性都是不可取的。它们都会导致代码组织混乱、难于调试、不便修改。如果一个子程序具有不良的内聚性,那最好还是花功夫重新编写,使其具有更好的内聚性,而不是再去花精力精确地诊断问题所在了。因此,知道应该避免什么是非常有用的,下面就给出一些不可取的内聚性:


           过程上的内聚性procedural cohesion)是指一个子程序中的操作是按特定的顺序进行。一个例子是依次获取员工的姓名、住址和电话号码的子程序。这些操作执行的顺序之所以重要,只是因为它和用户按屏幕提示而输入数据的顺序相一致。另一个子程序用来获取员工的其他数据。这段程序也具有过程上的内聚性,因为它把一组操作赋以特定的顺序,而这些操作并不需要为了除此之外的任何原因而彼此关联。

为了得到更好的内聚性,可以把不同的操作纳入各自的子程序中。让调用方的子程序具有单一而完整的功能:GetEmployee()就比Get­First­Part­Of­Employee­Data()更为可取。你可能还需要修改用来读取其余数据的子程序。为了让所有的子程序都具有功能上的内聚性,对两个或更多的原有子程序进行修改是很常见的。

           逻辑上的内聚性logical cohesion)是指若干操作被放入同一个子程序中,通过传入的控制标志选择执行其中的一项操作。之所以称之为逻辑上的内聚性,是因为子程序的控制流或所谓“逻辑”是将这些操作放到一起的唯一原因——它们都被包在一个很大的if语句或case语句中,而不是因为各项操作之间有任何逻辑关联。认为是逻辑上的内聚性的标志性属性就是各项操作之间没有关联,因此,似乎更应称其为“缺乏逻辑的内聚性”。

这方面的一个例子是名为InputAll()的子程序,它根据传入的控制标志来决定是输入客户姓名,员工考勤卡信息,还是库存数据。类似的例子还有Compute­All()Edit­All()PrintAll()SaveAll()。这种子程序的主要问题是你不该通过传入控制标志来控制另一个子程序的处理方式。相比之下,让三个子程序分别完成不同的操作,要比用一个“根据传入的控制标志选择执行三项不同的操作之一”的子程序清晰得多。如果这些操作中含有一些相同代码或共用了数据,那么应该把那些代码移入一个低层子程序中,这些子程序也应该包裹在一个类中。

如果子程序里的代码仅由一系列的if语句或者case语句,以及调用其他子程序的语句组成,那么创建这样一个具有逻辑上的内聚性的子程序通常也是可以的。在这种情况下,如果子程序唯一的功能是发布各种命令,其自身并不做任何处理,这通常也是一个不错的设计。这类子程序的技术术语便是“事件处理器(event handler)”。事件处理器通常用在各种交互性的环境中,例如像Apple MacintoshMicrosoft Windows及其他一些GUI(图形式用户界面)环境。

           巧合的内聚性coincidental cohesion)是指子程序中的各个操作之间没有任何可以看到的关联。它也可称为“无内聚性”或“混乱的内聚性”。本章开头给出的那个拙劣的C++子程序就具有巧合的内聚性。很难从巧合的内聚性转变为任何一类更好的内聚性——通常你需要进行深入的重新设计和重新实现

这些术语中没有哪个是神秘的或者神圣不可侵犯的。需要理解的是其中的想法,而不是那些术语。编写具有功能上的内聚性的子程序几乎总是可能的,因此把注意力集中于功能上的内聚性,从而得到最大的收获。

查看所有评论(0)条】

最近评论



正在载入评论列表...
热点评论