“一图胜千言”是一句英语谚语。它表达的是:一个复杂的想法可以用一幅静态图像传达;或者说,一个主题的一幅图像,比文字描述更能有效地传达其含义或精髓。
这句谚语同样适用于软件编程。确实,研究一个小型项目的源代码就能轻松理解它。然而,大型项目很快就会变得复杂而难以理解。在这种情况下,最好使用图表将源代码可视化,让开发者更容易理解。
让我们来探索CppDepend提供的一些有助于理解代码库的图表。
一、树状图(Treemap)
树状图是一种用嵌套矩形展示树形结构数据的方法。CppDepend 树状图使用的树结构就是常见的代码层次结构:
- C/C++ 项目包含命名空间,
- 命名空间包含类型,
- 类型包含方法和字段。
在树状图中,矩形代表代码元素。层级(level)选项决定每个单元矩形代表哪类代码元素。层级选项可以取 5 个值:项目、命名空间、类型、方法和字段。下面两张截图展示了同一个代码库,左边按类型层级显示,右边按命名空间层级显示。

树状图的“代码度量”选项决定矩形的大小。例如,如果层级设置为类型,度量设置为代码行数,那么每个单元矩形代表一个类型,单元矩形的大小与对应类型的代码行数成正比。
如果当前正在编辑一条 CQLinq 查询,查询匹配到的代码元素集合会在树状图上显示为一组蓝色矩形。在下面的截图中,一条 CQLinq 查询匹配了最大的 200 个方法。这里树状图层级是方法,度量为代码行数。不出所料,可以看到蓝色矩形代表的正是树状图中最大的 200 个单元矩形。
下面的截图还显示,当前选中的代码元素(这里是项目XML)在树状图上显示为红色矩形。

常见场景
通过选择恰当的度量与层级组合,度量视图能帮助您看到用其他方式难以发现的模式。过大、过复杂:CppDepend 提供了许多代码度量,用于发现过大和过于复杂的代码。应避免参数、变量、代码行数过多或圈复杂度过高的方法;代码行数过多的类型亦然。
在下面的截图中,树状图层级设为类型,度量为代码行数。大矩形代表代码库中的大型类型。将鼠标悬停在矩形上会显示该类型的度量值——这里是类型的代码行数。显然,代码树状图不仅有助于发现过大和过于复杂的代码元素,还有助于比较它们各自的大小和复杂度。

缺陷定位
在下面的截图中,我们使用一条 CQLinq 查询找出参数和变量过多的方法。蓝色矩形在树状图上标出匹配的方法。这里有趣的一点是:一些匹配的方法似乎聚在一起。由于树状图是层级视图,如果一组代码元素聚在一起,就意味着它们属于同一个父级。事实上,这里聚在一起的 12 个方法都属于同一个类型。
如果没有树状图,要找出这 12 个方法的父类型会很困难。现在,代码质量评审人员可以把注意力集中在这个区域——它似乎比代码库的其他部分包含更多缺陷。

自顶向下的代码探索
CppDepend 的树状图度量视图支持放大和缩小。这让聚焦于某个特定的项目、命名空间或类变得很容易。这对于按代码行数探索和比较各组件的规模尤其有用。
生产力绝不应该用代码行数来衡量。然而,在特定组织的上下文中,统计代码行数已被证明是准确估算软件规模的有用度量。代码库的功能被组织进程序集、命名空间和类型中。通过树状图,这些产物变成了并排排列的矩形,矩形面积——也就是功能权重——可以直观地比较。能够定期通过树状图考察代码规模,是建立对功能权重和功能成本的准确认知的独特方式。
试着把一个您非常熟悉的代码库可视化成树状图,您会惊讶地发现一些意想不到的功能规模。

代码结构观察
选择使用体积(代码行数、方法的参数数量……)或复杂度之外的度量来查看度量视图,可以揭示一些有趣的观察结果。
排名度量(ranking metric)是一种衡量某个类型或方法在代码库中受欢迎程度的代码度量。将树状图与排名度量结合使用,可以清楚地看到热门类型声明在哪里。这能快速揭示代码库中什么最重要。
下面的截图展示了Microsoft DLR(动态语言运行时)代码库按排名度量绘制的树状图。树状图表明CallSite和Expression是 DLR 中的热门概念,事实也确实如此。
用其他代码度量也可以做出同样有趣的代码结构观察,比如传入/传出耦合或层级。

二、依赖图
CppDepend 提供了丰富的功能,帮助用户使用依赖图探索现有的代码架构。下面是最常用的代码探索场景:调用图
CppDepend 可以通过两步流程生成您需要的任何调用图。
- 第一步:查询一个类型、字段、方法、命名空间或项目的直接和间接调用方/被调用方。其效果是生成以下 CQLinq 查询,匹配所有请求的直接或间接调用方/被调用方。

- 请注意,在 CQLinq 查询结果中,度量DepthOfIsUsing/DepthOfIsUsedBy显示了使用深度(1 表示直接使用,2 表示使用了某个直接使用方,以此类推)。CQLinq 查询可以轻松修改,只匹配满足特定使用深度条件的间接调用方/被调用方。还要注意,所请求的调用方/被调用方不一定与相关代码元素属于同一种类。例如,这里我们查询的是直接或间接使用某个类型的方法。
- 第二步:当 CQLinq 查询匹配到用户想要的调用方/被调用方集合后,可以将匹配结果集导出到依赖图,从而显示所需的调用图。

类继承图
要显示类继承图,必须应用上一节(生成调用图)所示的同样两步流程。
- 第一步:生成一条 CQLinq 查询,查询继承自某个特定类(或实现某个特定接口)的类集合。这里会生成以下 CQLinq 查询:

- 第二步:将 CQLinq 查询结果导出到依赖图,以显示所需的继承图。

耦合图
您可能需要确切地知道某个依赖关系涉及哪些代码元素,特别是当您需要预估结构性变更的影响时。在下面的截图中,CppDepend 的信息面板描述了两个项目之间的耦合。
指向依赖矩阵中的一个单元格,它会告诉您:项目 A 的 X 个类型正在使用项目 B 的 Y 个类型。请注意,您可以更改单元格权重(Weight on Cell)选项,改为方法数、成员数或命名空间数,以便用类型之外的东西来衡量耦合。

只需左键单击矩阵单元格,就会显示下面的耦合图。

耦合图也可以从依赖图中的一条边生成。在这里,您可以把边粗细(Edge Thickness)选项调整为类型数。

路径图
如果您想调查两个代码元素之间的路径或依赖环,首先要做的是显示依赖矩阵,并选择选项单元格权重:直接与间接使用深度。
蓝色和绿色矩阵单元格将代表路径,而黑色单元格将代表依赖环。例如,这里的信息面板告诉我们,涉及的两个类型之间存在一条最小长度为 7 的路径。

只需左键单击该单元格,就会显示下面的路径图。

全部路径图
在某些情况下,您需要知道从代码元素 A 到代码元素 B 的所有路径。例如,这里的信息面板告诉我们,涉及的两个类型之间存在一条最小长度为 2 的路径。


最后,将 CQLinq 查询匹配到的 12 个类型导出到图中,就能显示从 A 到 B 的所有路径。

环图
正如我们在上一节所解释的,要处理依赖环图,首先要做的是显示依赖矩阵,并选择选项单元格权重:直接与间接使用深度。此时黑色单元格代表环。
例如,这里的信息面板告诉我们,涉及的两个类型之间存在一个最小长度为 5 的依赖环。

只需左键单击该单元格,就会显示下面的环图。

请注意,能得到像上面那样干净、“圆润”的依赖环是例外而非常态。
通常,展示一个环会得到像下面这样不“圆润”的图。在这个例子中,涉及的两个类型(黄色)之间环的最小长度是 12。数一下从一个黄色类型到另一个黄色类型所经过的边数,您会得到 12。您会发现有些边被计算了不止一次。

三、依赖结构矩阵(DSM)
用依赖结构矩阵可视化大型图
在这里,我们想强调一点:当依赖图变得难以阅读时,值得切换到依赖矩阵。依赖图和依赖矩阵之所以共存,是因为:
- 依赖图很直观,但只要节点之间的边太多,就会变得难以阅读。
- 依赖矩阵需要花时间理解,但一旦掌握,您就会发现,在探索现有架构方面,依赖矩阵远比依赖图高效。关于依赖矩阵可读性的更多信息,请参见一眼识别代码结构模式
为了说明这一点,下面用依赖图和依赖矩阵分别展示了同样 77 个命名空间之间的依赖关系。


DSM(依赖结构矩阵)是表示和浏览组件之间依赖关系的一种紧凑方式。对大多数工程师来说,谈到依赖关系,想到的就是这样的东西:

DSM 用来表示与图相同的信息。
- 矩阵的表头元素代表图中的方框
- 矩阵中的非空单元格对应图中的箭头。
因此,在下面的快照中,从Net到Foundation的耦合,在矩阵中由一个非空单元格表示,在图中由一条箭头表示。

为什么要用两种不同的方式——图和 DSM——来表示同样的信息?因为两者各有利弊:
- 图更直观,但当节点和边的数量增加时,可能变得完全无法理解(几十个方框就足以产生一张过于复杂的图)
- DSM 不那么直观,但在表示大型复杂图时非常高效。我们说 DSM 比图更具可扩展性。
一旦理解了 DSM 的原理,人们通常会更喜欢用 DSM 而非图来表示依赖关系。这主要是因为 DSM 让人能一眼发现结构模式。本文档后半部分会对此作出解释。
CppDepend 提供上下文相关帮助,引导用户理解在 DSM 上看到的内容。CppDepend 的 DSM 对单元格采用简单的三色方案:蓝色、绿色和黑色。当鼠标悬停在某一行或某一列上时,上下文相关帮助会解释这种配色方案的含义:

非空的 DSM 单元格包含一个数字。这个数字代表该单元格所表示的耦合强度。耦合强度可以用耦合所涉及的成员/方法/字段/类型或命名空间的数量来表示,具体取决于单元格权重选项的当前值。除了上下文相关帮助,DSM 还提供一个信息面板,用平实的语言描述耦合关系:

CppDepend 的 DSM 提供了众多可供尝试的选项:
- 它有大量深入探索依赖关系的功能(可以展开父列/父行,可以展开单元格……)
- 它既能处理方形对称 DSM,也能处理矩形非对称 DSM
- 水平和垂直表头可以绑定在一起,始终保持方形对称矩阵
- 它带有间接使用(Indirect usage)选项,单元格会显示直接和间接使用关系
- 垂直表头可以包含分层级的代码元素
- ……
建议您通过分析自己代码库中的依赖关系,亲自尝试所有这些功能。在矩阵上识别代码结构模式
正如引言所解释的,DSM 的特点是能轻松识别常见的代码结构模式。让我们介绍最常见的场景:分层代码。DSM 能直观呈现的一种模式是分层结构(即无环结构)。当矩阵呈三角形——所有蓝色单元格位于左下三角、所有绿色单元格位于右上三角——就说明该结构是完全分层的。换句话说,该结构不包含任何依赖环。

在快照的右侧,同样的分层结构用图来表示。所有箭头都是同样的从左到右方向。图的问题在于其布局无法扩展:在这里,我们勉强能看出结构的全貌;如果方框数量翻倍,图就会变得完全无法阅读。而 DSM 表示则不会受到影响;我们说 DSM 比图更具可扩展性。
附注:有趣的是,大多数图布局算法都依赖图是无环的这一事实。为了计算带环图的布局,这些算法会临时丢弃一些依赖关系,按分层图来处理,然后在计算的最后一步再把丢弃的依赖关系加回来。依赖环:如果结构包含一个环,该环会在 DSM 上显示为一个红色方块。可以看到,在红色方块内部,绿色和蓝色单元格沿对角线交错分布。还有一些黑色单元格,代表相互直接使用(即 A 使用 B 且 B 使用 A)。

CppDepend 的 DSM 带有独特的间接依赖(Indirect Dependency)选项。A 与 B 之间的间接依赖意味着:A 使用了某个东西,那个东西又使用了某个东西,某个东西又使用了某个东西……最终使用了 B。下面以间接模式显示同一个带环的 DSM。可以看到,红色方块内只填满了黑色单元格。这仅仅意味着:对于环中的任意元素 A 和 B,A 和 B 间接地相互依赖。

下面是用图表示的同一结构。红色箭头表明多个元素相互依赖。但图无法帮助突出显示父环中涉及的所有元素。

请注意,在 CppDepend 中我们提供了一个按钮,用于在 DSM 中突出显示环(如果有)。如果结构是分层的,这个按钮会将矩阵三角化,并让非空单元格尽可能贴近对角线。

高内聚——低耦合
高内聚(组件内部)/低耦合(组件之间)的理念如今很流行。但如果您无法度量和可视化依赖关系,就很难对内聚和耦合做出具体评估。DSM 擅长展示高内聚。在下面的 DSM 中,对角线周围显示出一个明显的方块聚合。这意味着方块中涉及的元素具有高内聚性:它们彼此强烈依赖。此外,由于没有环,我们可以看出它们是分层的。它们当然是归入某个父级产物(如命名空间或程序集)的候选。
另一方面,方块周围的大多数单元格都是空的,这表明方块中的元素与其他元素之间是低耦合的。

在下面的 DSM 中,我们可以看到两个高内聚的组件(上方和下方的方块),它们之间的耦合相当低。

在重构时,这样的指标非常有用,可以判断是否有机会把粗粒度的组件拆分成若干更细粒度的组件。
职责过多
单一职责原则(SRP)如今在软件架构师圈子里越来越流行。该原则指出:一个类不应有超过一个变更理由。对 SRP 的另一种解读是:一个类不应使用过多不同的其他类型。如果把这个思想推广到其他层级(程序集、命名空间和方法),那么可以肯定,如果一个代码元素使用了几十个不同的(同级的)其他代码元素,它就承担了过多的职责。人们常用“上帝类”或“上帝组件”来描述这样的代码。
DSM 可以帮助找出职责过多的代码元素。这样的代码元素表现为:其列中有许多蓝色单元格,其行中有许多绿色单元格。下面的 DSM 就展示了这种现象。

热门代码元素
热门代码元素是被许多其他代码元素使用的元素。热门代码元素不可避免(想想String类就知道了),但热门代码元素并不是缺陷。它只是意味着:在每个代码库中,都有一些由热门类所代表的核心概念
热门代码元素表现为:其列中有许多绿色单元格,其行中有许多蓝色单元格。下面的 DSM 突出显示了一个热门代码元素。

值得注意的是,当您让代码结构保持完美的分层时,热门组件自然会处于低层。事实上,热门组件实际上不可能使用很多东西:因为热门组件是低层的,它们不能使用更高层的东西。那会造成从低层到高层的依赖,从而破坏结构的无环性质。
相互依赖
您可以通过右键单击一个非空单元格并选择打开此依赖关系菜单,来查看两个组件之间的耦合。

如果打开的单元格是黑色的,如上面的快照所示(即 A 和 B 相互依赖),那么生成的矩形矩阵将同时包含绿色和蓝色单元格(可能还有黑色单元格),如下面的快照所示。

在这种情况下,您经常会注意到绿色或蓝色单元格的数量不对等(这里 3 个蓝色单元格对 1 个绿色单元格)。这是因为,即使两个代码元素相互依赖,它们之间通常也存在一个自然的层级顺序。例如,考虑System.Threading命名空间和System.String类。它们相互依赖——彼此都依赖对方。但矩阵显示,Threading对String的依赖远强于反向依赖(蓝色单元格远多于绿色单元格)。这印证了直觉:Threading的层级高于String。

