第10章 软件构架重构

 
 10.1介 绍
 
在本书中,我们始终把构架当作在很大程度上受您控制的亊物,并说明了如何制定构 架决策(第3部分将阐述如何分析这些决策进行),以实现待开发系统的目标和需求。但是,我们还需耍考虑另外一方面。假定个系统己经存在,但不知道其构架。也许最初 ;的开发人员从来没有编写过构架文档:也许曾编写过文档,但丢失了:还有可能是虽然编 写了文档.但对系统进行了系列更改,怛文档并没有及时更新。如何维护这样的系统? 如何管理其演变以维护其构架(无论它是哪种)为我们提供的质量属性?
 
本章使用构架重构讲述了回答这些问题的方法,其中,已实现系统的“已构建”的构架是从现有系统中获得的。这是通过使用支持工具对系统进行详细分析来完成的。这些工 具提取关于系统的信息,协助构建和聚合连续的抽象级别。如果工具很有效,最终就会得 到协助对系统进行分析的构架表示。在某些情况下,可能无法产生一个有用的表示。在遗 留系统没有可以恢复的内聚构架设计时便会出现这种情况(尽管从本质上说了解这一点是'有用的)。
 
构架重构是一种解释、交互和迭代的过程,涉及许多活动;它并不是自动进行的。它 需要反向工程专家和设计师具备相关技能并投入精力(或具备关于该构架的重要知识的某 个人),这在很大程度上是因为在源代码中并没有清楚地表示构架构件。对于层、连接器 或可以轻松地从源代码文件中挑选出来的其他构架元素,并没有编程语言构件-很少标记构架模式(如果使用的话)。相反,我们通过实现中的许多不同的机制实现构架构件,通 常是功能、类、文件、对象等的集合。最初开发系统时,其高层设计/构架元系被映射到实现元素上。因此,在重构这些元索时,就需要应用反向映射。随之而来的是要求能够对构架进行洞察。熟悉编译器构造技术和实用程序也很重耍.如grep. sed. awk. perl, python, lex/yacc。
 
可以用几种方法来使用构架重构的结果。如果构架没冇文档或已经过期.那么,可以 把恢复的构架表示作为重新编写构架文档的基础,第9章已经对此进行了讨论。还可以使用 该方法恢复 “as-built”构架,以根据“已设计的”构架检査符合性。这可以保证我们的维 护人员(或开发人员)遵循对其提出的构架规定。不损坏构架,分解抽象,桥接层,对信息隐藏进行折衷等。还可以使用重构作为分析构架的基础(参见第11章和第12章),或 作为对系统进行再工程以获得一个期望的新构架的起点。最后,可以使用表示确定用于重 用的元素,或建立基于构架的软件产品线(参见14章)。
 
构架重构己用在了各种项目中,从MRI扫描仪到公共电话交换机,从直升机导航系 统到分类的NASA系统。它已被用于:
 
•    为物理模拟系统重新编写构架文档
 
•    理解采矿机的嵌入式控制软件中的构架依赖关系
 
•    评价卫星地面系统的实现与其参考构架的符合性
 
•    理解汽车制造业中的不同系统
 
10.1.1工作台方法
 
构架重构需要工具的支持,但任何一个工具或工具集对进行构架重构都是不够的。首 先,工具往往是面向特定语言的.但在分析的制品中,我们会遇到很多种语言。例如,一个成熟的MRI扫描仪可能会包含用15种语言编写的软件。其次.数据提取工具是有缺点 的,它们经常返回不完整的结果或错误的证据,所以我们使用精心挑选的工具来进行补充, 并对彼此进行检查。最后,重构的目的是不同的,上面已对此进行了讨论。所恢复文档的 用途决定了需要提取什么信息,这反过来又会建议使用不同的工具。
 
合起来看,这些就形成了支持构架重构的工具集的特定的设计哲学,即我们所熟知的 “工作台”。工作台应该是开放的(易于根据要求集成新的工具),并提供一个轻量级的集 成框架,在这个框架中,向工具集中添加的工具不会不必要地影响现有的工具或数据。
 
工作台的一个例子是在SEI开发的Dali,我们将使用该工作台阐述本章的几个观点. 本章的结尾部分还描述了其他的工作台。
 
10.1.2重构活动
软件构架重构由以下活动组成,这些活动以迭代的方式进行
(1)信息提取.,该活动的目的是从各种源提取信息。
 
(2)数据库构造。数据库构造包括将该信息转换为标准的形式,如Rigi    Standard Form (一种基于元组的数据格式,形式是relationship<entityl><entity2>,以及用于创建数据库 的基于SQL的数据库格式。
 
(3)视图融合,视图融合将数据库中的信息组合在一起,以生成该构架的一个内聚 的视图。
 
(4)重构。在重构活动中,主要工作是构建数据抽象和各种表示以生成构架表示。
 
正如您町能已经预料到的,这些活动具有极高的迭代性。图10.1描述了构架重构活动 以及信息如何在这些活动中流动。
 
            图10.1构架重构活动(箭头展示了活动中的倌息流向)
 
重构过程需耍有一些人的参与。这些人包括一个进行重构工作的人(重构人员),以 及一个或多个熟悉所重构的系统的人(设计师和软件工程师)。
 
重构人员从系统中提取信息,然后以手工方式或使用工具从信息中提炼出构架。构架 由重构人员通过对系统做出-组假定来获得。这些假定反映了从源制品到设计的反向映射 (理想情况下就是设汁映射的反面)。通过生成反向映射并把它们应用到提取的信息中来 对结果进行验证,从而对假定进行测试。为了最有效地生成这些假定并对其进行验证。必 须让熟悉系统的人参与此项工作,包括参与系统开发的系统设计师或工程师(最初开发了该系统或现在正对其进行维护的人)。
 
在下面的各节中,将更详细地概述构架重构的各种活动,以及每个活动的指导方针。 大多数指导方针都不是专门适用于某个特定工作台的,即使手工进行构架重构,这些指导方针也是适用的。
 
10.2信息提取
信息提取涉及分析系统现有的设计和实现制品,以构造系统的模型。所得到的结果就 是放在数据库中的信息集合,在视图融合活动中.我们使用该信息集合来构造系统的视图》 信息提取就是理想与现实的混合。前者是您希望发现的关于构架的信息.这些信息将 帮助您实现重构工作的目标,后者是可用的工具实际能够提取和提供的信息。根据源制品 (例如代码、头文件、build文件)和其他制品(例如执行踪迹),您可以确定并捕获系统 中感兴趣的元素(例如文件、函数、变量)及其关系,以获得几个基本的系统视图。表10.1 给出了可以提取的元素以及它们之间的几个关系的典型列表。
                    表10.1所提取的典型元素和关系
 
元素之间的每个关系都提供了关于系统的不同信息。函数之间的calls关系能够帮助我 们构建•个调用阁。文件之间的includes关系为我们提供了系统文件之间的一组依赖.函 数和变量之间的access_read和access_write关系展示了使用数据的方式。某些函数可以编 写一组数据,其他函数可以读取这个数据集合。该信息用于确定数据在系统的各个部分之 间的传递方式。我们可以确定是否使用了全局变量存储,或者是否大部分信息都是通过函 数调用来传递的。
 
如果所分析的系统很大,那么,捕获源文件存储在目录结构中的方式对重构过程可能 很重要-可以将某些元素或子系统存储在特定的目录中,在以后试图确定元素时,捕获诸 如dir_contains_file和dir_contains_dir这样的关系是有用的„
 
所提取的元素集合和关系取决于所分析的系统的类型以及可用的提取支持工具。如果要重构的系统是面向对象的,那么,把类和方法添加到要提取的元素列表中,并提取和使 用•如 class is_subclass_of_class 和 class_contains_method 这样的关系。
 
可以把获得的信息按静态和动态进行分类..静态信息是仅通过观察系统制品来获得 的,而动态信息是通过观察系统的运行方式来获得的。我们的目的是把两者融合起来,以 创建更准确的系统视图(将在10.4节讨论视阁融合)。如果系统的构架在运行时发生了变 化(例如.系统在启动时读入了配置文件,其结果是载入了某些元素),那么,在进行重 构时.就应该捕获并使用该运行时配置。
 
为了提取信息,我们使用了各种工具,包括:
 
•    解析器(例如 Imagix, SniFF+. CIA, rigiparse)
 
•    抽象语法树(AST)分析器(例如Gen++, Refine)
 
•    语法分析器(例如LSME)
 
•    剖析器(例如gprof)
 
•    代码插装工具
 
•    流行工具(例如grep. perl)
 
解析器分析代码,并根据它生成内部表示(目的是生成机器代码)。然而在通常情况下,可以保存该内部表示以获得一个视图。AST分析人员进行了类似的工作,但他们建立 了所解析信息的显示树表示。我们可以构建遍历AST的分析工具,并以适当的格式输出 所选择的与构架相关的信息。
 
语法分析器对源制品进行分析,源制品纯粹是作为语法元素或环的字符串。语法分析 器的用户可以指定一组待匹配和输出的代码模式。与此类似,流行工具的集合(如grep 和perl〉可以执行模式匹配并在代码内搜索.以输出所需要的信息。所有这些工具——代 码生成解析器,基于AST的分析器,语法分析器和流行的模式匹配器——都用于输出静 态信息。
 
可以使用剖析和代码覆盖分析工具来输出关于所执行的代码的信息,通常并不涉及向 系统中添加新的代码。另--方面,在测试领域具有广泛适用性的代码插装涉及向系统中添 加代码,以在执行系统时输出特定信息。这些工具生成动态的系统视图。
 
还可以使用分析设计模型、build文件、makefiles和可执行文件的工具来根据需要提 取更多的信息。例如,build文件和makefiles包括存在于系统中的关于模块或文件依赖性 的信息,这些信息可能未反映在源代码中。
 
可以从源代码、编译时制品和设计制品中静态提取大量与构架相关的信息。然而,由 于后期绑定的原因,某些与构架相关的信息可能并不在源制品中。后期绑定的示例包括:
 
•多态性
 
•函数指针
 
• 运行时参数化
 
直到运行时才可以确定系统精确的拓扑。例如,多进程和多处理器系统使用诸如J2EE. Jini或.NET这样的中间件动态频繁地建立其拓扑,这取决于系统资源的可用性。此类系统 的拓扑并不存在于其源制品中,因此不能使用静态提取工具对其进行反向工程。
 
基于此原因,可能有必要使用能够生成关于系统的动态信息的工具(如分析工具)。 当然.这耍求能够在系统执行的平台上获得此类工具。此外,从代码插装中收集结果可能 是困难的。嵌入式系统通常无法输出此类信息。
 
指导方针
 
以下是在实际中应用该方法的这一步时应该考虑的一些事项:
 
• 用“最少的工作量”提取。考虑您需耍从源信息集合中提取什么信息?该信息在 本质上是不是属于语法方面的?它是否要求对复杂语法结构的理解?它是否要 求进行语义分析?在每种情况下,都可以成动地应用一种不同的工具。一般而言, 语法方法是最经济的方法,如果重构目标很简单.应该考虑采用这种方法。
 
•验证已经提取的信息。在开始融合或处理获得的各种视阁前,确保已经捕获了正 确的视图信息。确保用于分析源制品的工具在正确地工作是非常重要的-首先要 根据基础的源代码对元素和关系的子集进行分析和验证.以确定已捕获了正确的 信息。需要手工验证的信息的准确数量由您来决定。假定有一个统计样本,您可 以确定期望的置信度,并选择样本策略来实现它。
 
•在需要的地方提取动态信息,如有大景运行时或后期绑定,以及可动态配置构架 的地方。
 
10.3数据库构造
 
在数据库构造期间,将提取的信息转换为标准的格式以存储在数据挥中。有必要选择 -个数据庳模型。在选择数据库模型时,请考虑如下问题:
 
•它应是-个众所周知的模型,以用另.个相对简单的模型代替一个数据库实现
 
•它应允许进行高效的査询,考虑到该源模型可能会相当大,这一点显得非常重要,
 
•它应支持从分布在不同地理位置的一个或多个用户界面对数据库进行远程访问
 
•    它通过组合来自各个表的信息来支持视图融合。
 
•    它支持可以表示构架模式的查询语言。
 
•    实现应该支持检査点,这意味着可以保存中间结果。在迭代过程中,这一点非常
重耍,因为它总是能够使用户撤销所做的修改。
 
例如,Dali 工作台使用关系数据库模型。它将提取的视图(这些视图可能会以很多不 同的格式存在,这取决于提取视图所使用的工具)转换为Rigi Standard Form.然后,由 perl脚本读入该格式,并以一种包含必要的SQL代码的格式输出,以构建关系表,并在表 中填充所提取的信息。图10.2概述了这一过程„
                                    图10.2将提取的信息转换为SQL格式
构建并填充关系表的所生成的SQL代码的示例请看图10.3„
create table calls( caller text, callee text ); 
create table access( func text, variable text ); 
create table defines_var(file text, variable text );
...
insert into calls values( 'main','control ');
insert into calls values( 'main', 'clock');
...
insert into access  values( 'main'、 'stat');
        图10.3    用Dali生成的SQL代码的示例
 
当把数据输入数据库时,生成了两个额外的表:元素和关系。这两个表分别列出了所 提取的元素和关系..
 
在这里,工作台方法使得可以采用新工具和技术(除了当前可以获得的工具和技术) 将提取工具所使用的格式转换为其他格式。例如,如果要求使用某个工具來处理新的语言, .那么,就可以对其进行构建,并将其输出转换为工作台格式
 
在Dali工作台的当前版本中,POSTGRES关系数据库通过使用SQL和perl来提供生 成和处理构架视图的功能性(10.5节给出了示例)。可以轻松修改SQL脚木.以使它们与 其他SQL实现兼容。
 
指导方针
 
在构造数据库时,请考虑如下问题:
•    根据提取的关系构建数据库表,以使视图融合期间数据视阁的处理更加容易。例 如,构建一个存储特定查询结果的表,避免要再次运行查询。如果需要这些结果. 可以很容易地通过访问该表获得它们。
 
•    对于任何数据库构造,在开始构造前仔细分析数裾库设计。主(可能是辅)键是什么?任何数据库连接都需要扫描多个表吗?在重构中,表通常是相当简单的
(顺序是 dir_contains_dir 或 function_calls_function)主键是整行的一个函数。
•    使用像perl和awk这样的简单语法工具,将用任何工爲所提取的数据的格式变为 可以由工作台所使用的格式。
 
10.4视图融合
 
视图融合包括定义和处理所提取的信息(现在存储在数据库中),以协调、加强并建 立元素之间的连接。不同形式的提取应该提供互补的信息。下面儿节通过示例对融合进行 了说明。
 
10.4.1改进视图
 
考虑一下图10.4中所示的两个代码段,它们来自于从用C++实现的系统中提取的方法 集(每个方法前都给出了各自的类)。这些表包括面向对象代码段的静态和动态信息。例 如,我们可以从动态信息中看出调用了 List::getnth。然而,该方法并不包含在静态分析中, 因为静态的提取器工具遗漏了它。此外,对InputValue和List的构造器和析构器方法的调 用并不包含在静态信息中,需要添加到协调两个信息源的类/方法表中。
 
此外,该示例中的静态提取展示了 PrimitiveOp类有一个称为Compute的方法,动态 提取结果中没有此类,但它们确实展示了类,如ArithmeticOp. AttachOp和StringOp,其 中每个类都有一个Compute方法,实际上该方法是PrimitiveOp的-个子类。PrimitiveOp 纯粹是•个超类,因此实际上在执行程序中从来没有调用过。但是在扫描源代码时.静态 提取器肴到的是对PrimitiveOp的调用,因为对PrimitiveOp的一个子类的多态调用是在运 行时发生的。
 
为了获得构架的准确视图,我们需要协调PrimitiveOp的静态和动态信息。为了做到 这一点,我们使用SQL查询对提取的calls, actually_calls和has_subclass关系进行融合。 这样,我们可以看到对PrimitiveOp::Compute (从静态信息中获得)及其各种子类(从动 态信息中获得 的调用实际上是相同的。
            图10.4关于class_contains_method关系的静态和动态数据信息
 
图10.5中的列表展示了添加到融合视图中的项(除了静态和动态信息达成一致的方 法)以及从融合视图屮删除的项(尽管包含在静态或动态信息中)。