软件学报
JOURNAL OF SOFTWARE
2000　Vol.11　No.4　P.557-562




一种利用模块内聚性的对象抽取方法
周毓明　徐宝文
　摘要　引入子程序-类型关系图来表示程序中类型和子程序之间的关系,讨论了模块内聚性的几个度量准则，并分析了增删子程序对模块内聚度的影响.在此基础上，给出了基于模块内聚性的对象抽取算法.
　关键词　对象,对象抽取,内聚性,紧密度,重叠度.
　中图法分类号　TP311
An Object-Extracting Approach Using Module Cohesion
ZHOU Yu-ming　XU Bao-wen
（Department of Computer Science and Engineering　Southeast University　Nanjing 210096）
Abstract　　In this paper, an St (subprogram-type) graph is introduced to represent the relation between subprograms and types in programs. Several module cohesion metrics are discussed, and the effects of adding a subprogram to or deleting a subprogram from a module are analyzed based on module cohesion. An object-extracting algorithm is proposed.
Key words　Object, object-extracting, cohesion, tightness, overlap.
　　用面向对象技术开发的的软件,程序理解容易,后期维护方便.但现存的许多软件的开发语言或是非面向对象语言,或尽管是面向对象的语言但开发时没有使用面向对象的技术,再加上有些软件没有良好的编程结构,导致软件后期维护十分困难.因此,如果能在这样的系统中抽取出子程序与数据之间的关系,尽可能地还原出系统设计人员头脑中的“对象”,并将其转换成用面向对象技术开发的系统,则可提高系统的可靠性、可维护性和可重用性.
这项工作的关键是通过对程序行为的详细分析,抽取出相关的数据和子程序,组合成有实际意义的对象.尽管在这方面已有不少工作［1～4］,如Liu和Widle提出的基于全局变量和基于类型的方法［1］、Panose等人提出的基于接收器的方法［3］,但这些方法只是将数据和子程序按类型或全局变量简单的分类,没有考虑模块的性质,抽取出的对象有可能与实际中的对象不符.Canfora在Liu和Widle的方法的基础上提出了用子图的内部联结度的方法［2］,但该方法实际上还是利用全局变量来分类的,忽略了简单类型与复杂类型在对象抽取过程中的作用不同这个事实.为克服这些不足,本文提出一种基于模块内聚性的对象抽取方法.
1　子程序-类型关系图
　　正如人们所熟知的,子程序包括过程和函数,过程常带有形参声明,函数有形参声明和返回值类型声明,有时,子程序体内还涉及到全局变量的处理,因此子程序与形参类型、返回值类型和全局变量的类型紧密相关,子程序之间因这些类型而关联.在对象识别过程中,各类型的作用是不一样的,复杂类型比简单类型的作用大.为刻画类型的复杂程度,下面先引入类型复杂度的概念.如果类型t1被用来定义类型t2,则称t1是t2的一个成分类型.类型t的复杂程度Ct,l与它的所有成分类型的复杂程度和成分类型相对于t的嵌套层次数l有关(t相对于本身的层次数是0),其定义如下：

其中v为基本类型的复杂度,t′为指针所指向的类型,Complex为指针类型的复杂程度.f表示类型t的复杂程度是其所有成分类型t1,t2,...,tn的复杂度Ct1,l＋1,Ct2,l+1,...,Ctn,l+1的函数,它可随具体的语言而定.例如,在Ada中,当t为记录类型时,f可取∑Cti,l+1(ti为记录的成分类型);当 t为数组类型时,f可取dim*2*Ct′,l+1(因为数组类型t的复杂性与它的维数d密切相关,但与数组元素具体数目没有太大关系.t′为数组元素类型,这里乘2的原因是一维数组的复杂度显然比只有一个成分的记录复杂,经考虑乘2较合适).在此基础上有如下定义：
　　定义(子程序-类型关系图). 令Ssubp表示系统中的子程序集,Ssubp表示系统中的类型集.如果图G=(N,E)中N=Ssubp∪Stype,E=｛(p,t)｜p∈Ssubp∧t∈Ssubp∧t为p中涉及到的类型｝,任一条边(p,t)上的权值为Ct,0,则称图G为子程序-类型关系图.
　　 对图G中的任一子程序结点p,记其涉及的类型集为Sp-t(p),即Sp-t(p)=｛t｜t∈Stype∧(p,t)∈E｝;记与任一类型结点t有关的子程序集为St-p(t),即St-p(t)=｛p｜p∈Ssubp∧(p,t)∈E｝.
　　这样,系统中子程序与类型之间的关系就体现在子程序-类型关系图上,最简单的对象识别方法就是在子程序-类型图上找出一个个独立的子图,每个独立的子图就构成一个对象.如果按这种方法抽取对象,有可能识别出的对象没有实际意义.这主要有两种情况.一种是将逻辑上属于不同对象的子程序封装在同一个对象中.如,栈对象有Push-Stack (S: in out STACK-TYPE;Elem: in ELEM-TYPE)子程序,队列对象有Enter-Queue (Q: in out QUEUE-TYPE; Elem: in ELEM-TYPE)子程序,在子程序-类型关系图上,这两个子程序因涉及到共同的类型ELEM-TYPE而相连,按上面的方法它们就封装在同一个对象中.另一种是将逻辑上属于不同对象的属性封装在同一个对象中.如，子程序Init (S: in out STACK-TYPE; Q: in out QUEUE-TYPE)完成栈对象和队列对象的初始化,栈和队列在子程序-类型关系图上因子程序Init而在同一个子图中,按上面的方法就将栈和队列封装在同一个对象中.在这些情况下抽取出的对象显然没有实际意义.如果利用模块的内聚性来抽取对象,则可以避免上述无实际意义的对象的出现.为此,下面对模块的内聚性加以分析.
2　模块内聚性分析
　　模块由类型集以及操纵这些类型的子程序集组成,模块内子程序之间因涉及到对相同的类型处理而存在关联关系.模块的内聚性可用模块内部元素之间结合的紧密程度来刻画,从系统中抽取出的模块应符合软件工程高内聚、低耦合的原则,即抽取出的模块应具有较高的独立性,联系密切的元素不应分布在多个模块中,而应划分到同一个模块中.
对子程序-类型关系图G=(N,E),令MTint(G)表示图G中所有子程序共同涉及到的类型,即
MTint(G)=∩p∈SsubpSp-t(p).
MTint(G)描述了模块内所有子程序之间涉及到的共同类型集.在下文中,为方便起见,用｜S｜表示任何种类的集合S中的元素个数.为了描述表示模块的内聚度,下面引入模块紧密度、重叠度和内联度的概念.
　　模块G的紧密度Tightness(G)用模块内所有子程序共同涉及的类型MTint(G)的相对复杂性之和与模块涉及的所有类型Stype的相对复杂性之和的比值来表示.模块G有一个高紧密度意味着模块G内各子程序涉及的共同类型比较多或涉及到的类型的复杂度比较高,即这些子程序紧密相关,这表明模块的内聚程度较高.亦即

显然,当MTint(G)=Stype(即模块内所有子程序涉及的类型都相同)时,模块的结合度达到最大值1.
　　模块G的重叠度Overlap(G)表示在平均一个子程序涉及的类型中，MTint(G)所占的复杂性比值,即在一个子程序中对公共部分处理的平均程度.重叠度高表明子程序与模块的大多数类型有关,从某方面反映了子程序间的高度关联关系,这也是模块具有高内聚的表现.由此，

Canfora在他的对象抽取方法中,为变量引用图中的子程序提出了子程序内部联结度的概念,然后根据各子程序的内部联结度对变量引用图作相应的合并、切片和删除,最后得到许多独立的子图,每个独立子图都视为一个候选对象.但他的对象抽取方法仅与子程序处理的变量个数有关,而与变量的复杂程度无关,这显然会带来一些问题.由于我们使用的是子程序-类型关系图,且考虑到子程序之间的关联程度不仅与它们处理的类型个数有关,而且与类型的复杂程度密切相关,因此，必须对Canfora的子程序的内部联结度重新定义.
　　我们将子图的内部联结度定义为两个顶点都在子图内的所有边上的权值之和与至少有一个顶点在子图内的所有边上的权值之和的比值.这里,边上的权值用该边一端的类型顶点的复杂度计算.子程序p的内部联结度是指由涉及的类型是Sp-t(p)子集的子程序集与类型集Sp-t(p)组成的子图的内部联结度.因此,我们得到子图内部联结度IC(p)的形式定义如下:

其中P(p)=｛pi｜Sp-t(ppi)Sp-t(p)｝.
　　模块的紧密度、重叠度和子程序内联度是模块内聚性的反映,它们的值越大,表明模块的内聚度越高.有了这些度量手段,用下一节的算法就可以在子程序-类型关系图中找出这样的一些子图:它们是由类型结点和子程序结点组成的子图,具有较高的内聚性,并且在一定程度上可以避免将逻辑上属于不同对象的子程序封装在同一个对象内，或将逻辑上属于不同对象的属性封装在同一个对象中.
3　对象抽取方法
3.1　增加和删除子程序对模块内聚度的影响
　　由于在子程序-类型关系图上,抽取过程中子图需要进行合并和分裂,这时需判断向模块增删子程序对模块内聚度的影响.为此,下面我们先定性分析向模块增加和删除子程序给内聚度造成的影响.设添加或删除的子程序名为p′,其涉及的类型为Sp-t(p′),改变后的模块记为G′.
3.1.1 向模块内增添一个子程序对模块内聚度的影响
当向模块G内添加一个子程序p′时,由模块紧密度、重叠度及MTint的定义可直接推得以下两个引理.
　　引理1. 

引理2. 

　　引理1表明,在模块所共同涉及的类型的复杂度增值与新增子程序涉及的类型复杂度之和的比大于等于原模块的紧密度时,新模块的紧密度才不会变小.引理2表明，模块重叠度的变化取决于新增子程序涉及的新模块中的公共类型的复杂度之和与其涉及的所有类型复杂度之和的比值与原模块重叠度之间的关系.而由模块内联度定义,显然，当Sp-t(p′)∩Sp-t(p)=Φ时,内联度不变；当Sp-t(p′)Sp-t(p)时,内联度变大；在其他情况下,内联度变小.
　　由上面的分析易知，当新添子程序涉及的类型包含了原模块涉及的公共类型时,如果新增子程序涉及的公共类型的相对复杂性与其涉及的总类型复杂性之比大于原模块的紧密度或重叠度,则模块的内聚度会保持不变或增大.即有如下推论.
　　推论1. 当MTint(G)Sp-t(p′)时,
　　① 若
　　② 若≥Tightness(G).
特别地,当MTint(G)=Sp-t(p′)时,Overlap(G′)≥Overlap(G),Tightness(G′)≥Tightness(G). 
3.1.2 从模块内删除一个子程序对模块内聚度的影响
　　删除子程序与增添子程序的过程正好相反,当从模块内删除一个子程序p′时,由模块紧密度、重叠度及MTint的定义可直接推得以下两个引理.
　　引理3. 

　　引理4. 

　　子程序P内联度的变化则决于被删子各序P'涉及的类型与P涉及的类型之间的关系.当Sp-t(p')∩Sp-t(p)Φ时,内联度不变;当Sp-t(p')Sp-t(p),内联度变小,在其他情况下,内联度会变大.
　　由引理3可知,模块紧密度的变化取决于被删子程序造成的模块所共同涉及的类型复杂度的养活量和它所涉及类型的复杂度的比值下原模块紧密度之间的关系.引理4则说明,模块的重叠度可能增大也可能减小,它取决于被删子程序涉及的新模块中的公共类型的复杂度之和下其涉及的氖类型复杂度之和的比值下原模块重叠度之间的函数关系.易知,当删除子程序p'后,模块涉及的公共类型没改变时,如果被删子程序p'涉及的原模块公共类型的相对复杂性与涉及的总类型复杂性之比小于原模块的紧密度或重叠度时,则模块的内聚度会保持不变或增大.即有如下推论.
推论2. 当MTint(G)=MTint(G′)时,
　　① 若
　　② 若
　　上面讨论了怎样提高一个候选模块的内聚度的充分条件,定性地分析模块的内聚度和模块的改变对内聚度的影响,从而为候选模块的抽取提供了合并与分裂的原则.下面,我们在此基础上讨论对象抽取算法.
3.2　对象抽取算法
　　子图之间存在一致性连接(coincidental connection)和假连接(spurious connection)两种联系方式［2］.一致性连接指的是一个子程序因实现多个功能而存取多个数据结构,每个功能逻辑地属于一个对象.这时，可将子程序分割成多个子程序,每个子程序实现一种功能,逻辑地属于一个对象.如，子程序Init完成栈和队列的初始化,这时可将Init分割成两个不同的子程序,分别实现对栈和队列的初始化.而假连接是子程序实现特定的功能而存取不同的数据结构.如,子程序Copy将栈中的元素复制到队列中,这时将其分割没有意义,所以在处理中将该子程序移去.对子程序的程序流图进行分析,就可判断出该子程序是一致性连接还是假连接.
　　在子程序-类型图上,每个类型可得出对应的一个子图,计算这些子图的内聚度.由于抽取出的模块要具有较高的内聚度,所以只考虑那些内聚度大于阈值的子图.如果这些子图的子程序结点集的交集不为空,则应根据增加或删除子程序对这些子图内聚度的影响程度来判定该子程序划归哪一个子图.如果子图的类型结点集的交集不为空,则应判定是否应将这些子图合并.对象抽取算法如下:
　　(1) 在子程序-类型关系图中,对每个类型t,记由子程序集St-p(t)和类型集∪p∈St-p(t)Sp-t(p)组成的子图为Mt,计算Tightness(Mt)或Overlap(Mt)(由上面内聚度的分析可知,其值越大,表示该子图的内聚度越高),若Mt的紧密度或重叠度大于一给定阈值Step,则将Mt加入OverStep列表.
　　(2) 对任意的子图Mt1,Mt2∈OverStep,当两子图的类型结点的交集中包含类型t1,t2时,分如下几种情况处理:
　　① 如果Mt1,Mt2合并为一个新的子图Mt后,子图的内聚度大于阈值Step,则表明这两个类型是同一个对象中两个联系密切的重要属性,因此将合并后的子图Mt加入OverStep列表.重新生成子程序-类型关系图,在这个新的类型关系图上,类型结点t1,t2合并为一个新类型结点t,在原图上,所有从子程序结点指向类型t1,t2的弧在新图上都指向类型结点t.
② 如果Mt1,Mt2合并为一个新的子图Mt后,子图的内聚度小于阈值Step.这时,如果是某个子程序同时存取t1,t2而造成两子图Mt1,Mt2之间的假连接,则将这个子程序从Mt1和Mt2中同时去掉；如果是某个子程序造成的一致性连接,则将其分割成几个逻辑功能独立的子程序.
　　(3) 重复(2),直到对任意Mt1,Mt2∈OverStep,这两个子图的类型结点的交集中不同时包含类型t1,t2为止.
　　(4) 对任意两个子图Mt1,Mt2∈OverStep,设它们子程序结点的交集为Set.我们认为每个子程序在逻辑上只属于一个对象,因此，Set中的子程序应划分给不同的子图.记Mt1,Mt2中去掉Set中子程序结点及相关联的边后的子图分别为M′t1,M′t2.对Set中的任意子程序p,如果p划归M′t1或M′t2都导致其内聚度小于Step,则抛弃p.否则，如果有下面3种情况之一出现:
　　① p划归M′t1后其内聚度增加而划入M′t2后其内聚度减小;
　　② p划入M′t1后内聚度增加的程度大于划入M′t2后内聚度增加的程度;
　　③ p划入M′t1后内聚度减小的程度大于划入M′t2后内聚度减小的程度，
则p应划归M′1.将改变后的子图加入OverStep列表.
　　(5) 重复(4)，直到对任意Mt1,Mt2∈OverStep,这两个子图的子程序结点的交集为空为止.
　　(6) 将OverStep列表中的每个子图的类型结点作为属性,子程序结点作为方法封装成一个类.
　　算法中的阈值Step可通过对大量模块的内聚度统计分析后得到,也可作为一个参量来调节,以观察和分析不同内聚度层次对由系统中抽取出的对象的影响.
3.3　对象间继承关系的抽取
　　为便于抽取对象间的继承性,下面，我们非形式地引入一些基本概念.一个对象的类型用［li:Bi∈1..ni］表示,其中li是对象的方法名或属性名,Bi是li的类型.若用A＜:B表示类型A是类型B的子类型,则蕴含是：如果A＜:B且a:A,则a:B.蕴含的含义是一个类型的子类型的实例也是该类型的实例.如果两个对象所对应的对象类型之间存在子类型关系,则这两个对象之间存在蕴含关系,即一个对象继承了另一个对象的属性和方法.对象继承性的抽取过程就是找出相应对象类型的子类型关系的过程.对象类型的子类型关系的不同定义对应于对象间继承关系的不同层次.下面引入一个较强的对象类型子类型的定义:如果子类型关系Bi＜:B′i,i∈1..n成立,则有［li:Bi∈1..n+mi］＜:［li:B′i∈1..ni］.
　　抽取对象间的继承性分为下面几步：首先用上一节的算法在程序中抽取出对象,且形式化地表示出对象的类型;然后通过对象类型的演算找出对象类型间的子类型关系,抽取出对象间的继承关系;最后将对象类型及其继承关系转换为相应对象间的继承关系,因子对象继承父对象的属性和方法,所以转换过程中子对象的属性和方法可能要进行适当的增删.
4　结束语
　　从代码中抽取出抽象层次的对象,毕竟是从原程序出发,与设计人员头脑中的“对象”有一定的差距,况且程序本来就未必真正实施了设计人员真正需要的对象.但通过逆向工程的方法抽取出程序中的“拟对象”,不仅有助于理解原系统的设计思想,而且能提高软件的可维护性和代码的可重用性.Liu和Widle的基于全局变量和基于类型的方法以及基于接收器的方法没有考虑模块的性质,只是将数据和子程序按类型或全局变量简单地加以分类,有可能将本来属于同一个对象的方法或属性分散到多个模块中,从而造成所取出的对象有可能与实际中的对象不符Canfora的方法在某些情况下得出的结果可能不理想.本文从模块的内聚性出发,将关联程度高的子程序、类型组合成对象,从而抽取出的对象内部具有较高的内聚度.我们已结合“Ada逆向工程与软件维护支撑技术研究”课题开发出一个原型系统.它基本的功能是,从用Ada83开发的软件中识别并抽取出各种对象,并将其转换为Ada95程序,其中最重要的部分就是服务性任务到保护对象的转换［5］.在将来的工作中，我们期望在对象抽取的实现中能加进语义知识的处理.
周毓明（东南大学计算机科学与工程系　南京　210096）　
徐宝文（东南大学计算机科学与工程系　南京　210096）
参考文献
1，Liu S S, Wilde N. Identifying objects in a conventional procedural language: an example of data design recovery. In: Proceedings of the IEEE Conference on Software Maintenance. San Diego, CA: IEEE Computer Society Press, 1990. 266～271
2，Canfora G, Cimitile A, Munro M et al. A reverse engineering method for identifying reusable abstract data types. In: Proceedings of the 1st IEEE Working Conference on Reverse Engineering. Baltimore, MD: IEEE Computer Society Press, 1993. 73～82
3，Livadas Panose, Johnson Theodore. A new approach to finding objects in programs. Journal of Software Maintenance: Research and Practice, 1994,6:249～260
4，Pedrycz Witold, Waletky James. Fuzzy clustering in software reusability. Software-Practice and Experience, 1997,27(3):245～270
5，Li Bang-qing, Xu Bao-wen, Yu Hui-ming. Transforming Ada severing tasks into protected objects. In: Proceedings of the ACM SIGAda Annual International Conference. Washington, DC: ACM Press, 1998. 240～245


