Visual C++ 中的依赖分析






4.44/5 (8投票s)
使用图表和指标分析静态架构
引言
在开发软件时,类之间的循环依赖等会使代码容易因修改而变得脆弱,如果它们存在于二进制文件之间,还会导致构建问题。通过源代码进行查找和修复可能非常耗时,因此本文使用开源分析工具 DeepEnds(Visual Studio 插件 / NuGet 包)分析了一些项目代码。
设置问题
要从菜单栏启动该工具,请选择“视图”→“其他窗口”→“DeepEnds”。
上图显示了左侧的输入和右侧的一些输出。请注意,解决方案资源管理器中项目文件的层次结构过滤器与 Visual Studio Community 2015 的 dgml 文件查看器中生成的图形之间的映射关系。请注意,Gherkin
过滤器包含功能文件,这些文件不会被 DeepEnds 解析,因为它们包含 DSL 代码(有兴趣的读者可以参考 A Slice of Cucumber)。
如果输入的是 .NET,则图中的节点将用命名空间和类的名称标记,相关的层次结构将形成子图。因此,这是选择过滤器名称的一种合理策略。请注意,跨项目重用过滤器名称时应谨慎,因为这可能导致隐藏二进制文件之间的循环依赖。
这样就留下了一个问题:哪个文件属于哪个过滤器。这导致了上图所示的相当牵强的示例。
图表是什么意思?
在 DGML 图中,TestFEA 节点已被展开以显示一个周期。通过查看相关的 HTML 报告(其中包含每个级别的摘要)可以更轻松地研究这个问题。顶级的报告的截断版本包含以下表格:
(E + P - N) / N | E + P - N | N | 外部 | SLOC | 环 | 节 | |||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
值 | 最大值 | Sum | 值 | 最大值 | Sum | 值 | 最大值 | Sum | Count | 最大值 | Sum | 预期效果 | 最大值 | ||
0.50 | 0.50 | 1.88 | 3 | 4 | 12 | 6 | 53 | 234 | 0 | 19 | 10672 | 30 | 469 | 顶层 | |
0.00 | 0.00 | 0.00 | 0 | 0 | 0 | 1 | 1 | 1 | 12 | 12 | 80 | 80 | 80 | App | |
0.00 | 0.27 | 0.27 | 0 | 3 | 3 | 2 | 11 | 17 | 0 | 3 | 513 | 29 | 99 | BLAS | |
0.44 | 0.44 | 0.61 | 4 | 4 | 5 | 9 | 53 | 124 | 7 | 16 | 4928 | 28 | 469 | FEA | |
0.00 | 0.00 | 0.00 | 0 | 0 | 0 | 52 | 52 | 52 | 2 | 3 | 2854 | 33 | 347 | Solvers | |
0.50 | 0.50 | 0.50 | 1 | 1 | 1 | 2 | 16 | 28 | 26 | 12 | 1958 | 36 | 432 | 环 | TestFEA |
0.00 | 0.00 | 0.00 | 0 | 0 | 0 | 3 | 3 | 6 | 19 | 19 | 339 | 11 | 203 | TestLinear |
前九列的公式基于边数 (E
)、部分数 (P
) 和节点数 (N
),这些在 Why Favour the Cyclomatic Number? 中进行了讨论。具体来说,树中该级别的值以及三个公式 (E+P-N)/N
、E+P-N
和 N
在树中的总和和最大值。
接下来的两列是外部数量的计数,对应于形成边的依赖项,以及在向下遍历树时其最大值。
然后给出遍历树时源代码行数的总和,接着是根据 Counting Lines of Code 中详述的对数正态分布进行拟合的结果,最后是树中的最大值。
最后两列包含我们示例中感兴趣的信息。是否存在周期以及它发生的(子)图。
导航到 TestFEA
部分的报告,有一个类似的表格:
(E + P - N) / N | E + P - N | N | 外部 | SLOC | 环 | 节 | |||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
值 | 最大值 | Sum | 值 | 最大值 | Sum | 值 | 最大值 | Sum | Count | 最大值 | Sum | 预期效果 | 最大值 | ||
0.50 | 0.50 | 0.50 | 1 | 1 | 1 | 2 | 16 | 28 | 26 | 12 | 1958 | 36 | 432 | 环 | TestFEA |
0.00 | 0.00 | 0.00 | 0 | 0 | 0 | 10 | 10 | 10 | 22 | 12 | 683 | 86 | 149 | TestFEA\App | |
0.00 | 0.00 | 0.00 | 0 | 0 | 0 | 16 | 16 | 16 | 18 | 8 | 1275 | 25 | 432 | TestFEA\Classes |
接下来是一个关于 26 个外部依赖项的表格,截断后如下所示:
外部依赖项 |
---|
BLAS\Algebra\Full.h |
BLAS\Algebra\Matrix.h |
BLAS\Algebra\Vector.h |
BLAS\FileIO\WriteMatrix.h |
FEA\Core\Elements\ElementHandler.h |
接下来,一个列出哪些文件形成边的表格:
内部依赖项 |
---|
TestFEA\App\stdafx.h |
TestFEA\Classes\AreaCalculator.h |
TestFEA\Classes\BoxMesh.h |
TestFEA\Classes\DiffFiles.h |
TestFEA\Classes\LocalNodes.h |
TestFEA\Classes\OneMesh.h |
TestFEA\Classes\TestSolver.h |
TestFEA\Classes\VolumeCalculator.h |
然后,一个详细说明哪些依赖项形成边的表格,截断后如下所示:
TestFEA\Classes | → | TestFEA\App |
---|---|---|
TestFEA\Classes\DiffFiles.h | → | TestFEA\App\stdafx.h |
TestFEA\Classes\LocalNodes.h | → | TestFEA\App\stdafx.h |
TestFEA\App | → | TestFEA\Classes |
TestFEA\App\GenerateGridTests.cpp | → | TestFEA\Classes\DiffFiles.h |
TestFEA\App\GenerateGridTests.cpp | → | TestFEA\Classes\BoxMesh.h |
TestFEA\App\GenerateTests.cpp | → | TestFEA\Classes\AreaCalculator.h |
TestFEA\App\GenerateTests.cpp | → | TestFEA\Classes\DiffFiles.h |
TestFEA\App\GenerateTests.cpp | → | TestFEA\Classes\LocalNodes.h |
最后,为完整起见,提供了一个包含结构矩阵的表格,因为它只是另一种报告图的方式。
App | \ | 1 |
---|---|---|
类 | 1 | \ |
修复问题
在查找和修复循环依赖时,选择的信息是形成边的底层依赖项。在这个牵强的例子中,可以看出问题是由于将 stdafx.h 放在 TestFEA\App 下而不是 TestFEA\Classes 下造成的。将其与 stdafx.cpp 和 targetver.h 一起移至 TestFEA\Classes 被证明能有效地消除循环。
讨论
如前所述,这个例子是牵强的。在早期的例子迭代中,代码有一个类具有两个职责,导致了一个循环,这在如何重构方面引起了极大的犹豫。具体来说,仅仅通过研究源代码并没有发现问题,这导致了最初(代码和随后)关于这个主题的文章 - As-Is Software Architecture。顺便说一句,通过将有问题的类分成两部分来打破了循环。请注意,由于所有代码都在同一个二进制文件中,因此没有构建问题。事实上,当从单个文件的级别来看时,并没有循环依赖,也就是说,问题只存在于(错误的)设计级别。
显然,基于文件的分析不如基于类的分析令人满意,后者可以通过 C# 和 Visual Basic 的 Roslyn 解析器实现。最初,添加了一个 Doxygen XML 解析器,但这导致了依赖项的明显遗漏。最近,为了解决这个问题,该工具添加了一个基于 libclang 的解析器。由于 libclang 是 clang 的一个稳定但有限的接口,因此存在一些特殊性。首先,由静态方法创建的依赖项会被忽略。其次,通过不完整的命名空间引用的类将被与类列表进行比较,如果只找到一个,则会创建一个依赖项——希望该列表是完整的。
历史
- 2016/10/07:首次发布
- 2016/10/12:更新表格
- 2016/11/25:添加了对 NuGet、Doxygen 和 libclang 的提及