架构开发环境(ADE)的探索与实践

在软件开发过程中,经常会遇到复杂的文件依赖关系,如同意大利面一样纠缠不清。为了解决这个问题,架构开发环境(ADE)应运而生。市场上存在多种ADE工具,如Structure101、Lattix、NDepend等。本文将介绍另一种ADE工具的实现,以及为什么需要开发新的解决方案。现有的解决方案在绘制依赖图时存在不足,无法满足对软件结构清晰度的需求。

ADE与普通的依赖图不同之处在于,它关联了一个层次结构,这个层次结构不一定与文件系统路径、命名空间或其他源代码工件相对应。最初,软件使用与Visual C++项目相关的过滤器文件(后面会给出示例并进行重构);随后,软件被泛化,可以扫描目录,也可以读取make命令的输出——在这两种情况下,用户必须手动指定一组过滤器。这些泛化的第一个可以用于展示软件本身的结构,后者是为了处理Linux内核的源代码而实现的。

软件实现是用Python编写的,使用插件来提供功能。这些插件通过处理JSON文件来加载和执行。输出选项包括:

  • doxygen - 创建Doxygen/DoxyPress的输入
  • track - 将指标追加到文件
  • dependencies - 保存包含来自其他输入选项之一的状态的JSON文件
输入选项包括:
  • visualstudio - 读取Microsoft Visual Studio解决方案文件,用于C/C++源代码和过滤器
  • generic - 遍历目录结构以获取源代码
  • maken - 读取"make -n"或"make V=1"的输出
  • dependencies - 读取保存的JSON文件,包含状态
例如,.NET程序集中的类和接口可以通过命名空间进行过滤。

支持不同的编程语言,包括C/C++、C#和Python,以及额外的处理。各种示例的源代码可以在仓库中找到。

背景理论:图由节点和连接它们的边组成,在有向图中,边有方向。图不仅可以作为图表表示,还可以通过一个称为结构矩阵的矩阵来表示。如果图中没有循环,那么可以选择行的顺序,使得非零项位于对角线的一侧。

给定一个图有N个节点,E条边和P个部分(图的不同部分之间没有边连接),则循环复杂度可以定义为E + P - N。例如,在严格分层的架构中,E + P - N = 0,对于完全混乱的情况,边的数量是N的三角形数的两倍,即E + P - N = N * N。因此,0 <= (E + P - N) / N <= N。注意,这个公式在代码转换下是不变的,这种转换将一个节点分成两个通过仅一条边连接的节点。

使用Doxygen进行可视化:首先,脚本计算包含文件的过滤器的结构矩阵,并尝试对行进行排序,使得矩阵是下三角的。注意,列的顺序与行相同,对角线元素由斜线表示。

脚本然后将过滤器合并成树结构。例如,创建了FEA\Core,它有3个子节点,分别是Elements、Fields和SetOfElements。然后它计算每个树节点的N、E + P - N和(E + P - N) / N的值,并创建一个按(E + P - N) / N排序的表格。

每个树的分支在HTML文档中创建一个新的部分。例如,在顶级,脚本创建一个输入文件,用于GraphViz创建第一个图表。每个节点都链接到文档中的适当部分。脚本还计算E + P - N,在这种情况下为1。

第二个图表显示了FEA子部分,并且架构中存在问题。FEA\Surface对FEA\Core的依赖在图中创建了一个循环,即存在循环依赖。HTML文档的相关部分还显示E + P - N的值为8,并补充说FEA本身依赖于线性代数库。

最后,在第三个图表中,展示了一个叶节点,即一个由文件的相互依赖组成的节点。注意这不是通常会显示的意大利面,因为只显示了少量的文件。

跟踪指标:如上所述,E + P - N的值为0对于简单图,因此跟踪所有生成的图的这个指标的总和是有用的。脚本跟踪这个值以及最大值,并将这两个值都归一化。

脚本plot.py绘制跟踪的值。对于示例所基于的代码,输出在complexity.txt中给出。需要注意的是,注释确实提到了移除不必要的包含文件和更改过滤器。

从上述图表中可以看出,重构代码的效果并不是指标的单调减少。它们的用途在于监控长期趋势。

讨论:Visual C++示例:对FEA::Core::Elements(原始、重构)过滤器进行了重大重构。ElementShape从IntegrationRule中分离出来,然后单独分组,没有在上一级创建循环依赖。即应用了SOLID的单一职责原则。

还有在ElementHandler中实现的工厂被分离出来,移动到ElementFactory,并再次移动到自己的组中,没有在上一级创建循环依赖。这样做的效果是为ElementHandler带来了一个清晰的继承树。即图表现在与设计意图一致。需要注意的是,这个特定的错误可能已经被认为是一个反模式。

应用程序自己的源代码:在Python源代码中使用了不必要的(在动态类型上下文中)导入语句,以便确定正确的依赖结构。

Linux内核:作为压力测试,软件在Linux内核4.6源代码上运行。显然没有预先存在的过滤器,而是有大量的文件和经典的分割include目录,将通用头文件与源代码分开。这一点很重要,因为需要重新分配include目录的全部内容,以便确定内核源代码的结构。

Boost 1.61库:这个C++库被选为示例,因为它曾经是一个只包含头文件的库,当想要使用一个很小的头文件时,需要大量的依赖,这造成了巨大的痛苦。在其现代形式中,它使用一些宏来定义包含文件,因此在读取源代码时,这些宏必须适当替换。

Mono.Cecil库:这个.NET库被选为示例,仅仅是因为读取其程序集的工具使用了它。由于工具不读取源文件,它创建了一个树,叶子对应于接口和类,分支对应于命名空间。

结论:已经开发并展示了一个用于生成相互关联的层次结构架构图和相关指标的工具,它产生了对特定Visual C++项目的更清晰的理解。在代码库被重构到更灵活的状态的程度上。该工具由于其插件架构而具有高度的可扩展性,并且已经被泛化,可以用于Python源代码、Linux内核和一般输入。

沪ICP备2024098111号-1
上海秋旦网络科技中心:上海市奉贤区金大公路8218号1幢 联系电话:17898875485