本文最初发布于 Alex Ellis 的个人博客。
有一天,我正在阅读关于个人电脑和桌面 GUI 发展的内容,我就想,我们都已经非常习惯个人电脑“桌面“这个类比。我们把文件放在文件夹里,并把它们放在桌面上。这个过程有很多物理动作发生。
人类非常善于理解空间,尤其是在记忆物理空间的时候,这让我联想到了我们通常如何将代码可视化。在思考和可视化代码的时候,有没有什么好的方法可以利用这一点?
这让我想到了我往往如何可视化代码,有点难以描述。我认为,它通常以不同的方式存在于我的脑海中,这取决于抽象和特殊性水平,而且同时存在若干不同方式的组合。根据需要,我可以很好地在它们之间切换,特别是当我对代码库非常熟悉的时候。例如,当我想象各种微服务之间的交互时,在每个服务的周围画个大方框是很有帮助的,把每个服务视为一个大的工作单元,彼此之间通过 RPC 交互。
一个简单的面向微服务的汽车租赁服务架构图,以及代表一条执行路径的分布式跟踪。
另一方面,如果对于计算机如何读取我给它的东西,我想知道微末的细节,那么把所有东西放大到物理内存表示是有帮助的。
我曾经做过的一个 google Sheets 页的截图,上面有内存地址和汇编指令。这对于非常仔细地了解整个过程很有帮助。
幸运的是,我把大部分时间都花在了中间的某个地方,在阅读实际的代码(比汇编高级),把大块的代码作为一个个大的单元来思考,和研究架构图及系统间通信之间做一些平衡。即使是代码本身也已经有了很多物理关系;想想目录路径、命名空间、行缩进以及代码行的线性排序。
对于这个问题,我考虑了一些不同的可视化技术,每一种技术都有不同的应用场景。考虑一下下面这个不完全的清单:
怎么对它们进行比较呢?首先,似乎有一个天然的抽象等级轴线,从低级的代码阅读到高级的架构图。似乎还有一些其他的轴线,我们也可以对它们进行排序。
也许我们可以根据它们在多大程度上代表了更大的系统来对它们进行排名?架构图在这方面做得很好,但火焰图只能代表单个执行路径。也许是变化的频率?这是一个有趣的问题,虽然源代码经常变,但架构图(有望)保持稳定。
让我们想一下,可视化如何很好地表示整个系统实际的代码执行情况呢?在这种情况下,高级的架构图得分也许不会很高,因为它抽象掉了服务框内的许多细节。分布式跟踪可以做得更好,不过具体程度取决于有多少个跟踪点。类图虽然有助于可视化类之间的关系,但可能并不表示实际的路径。火焰图可以显示清晰的执行路径,但只能显示单条代码路径,而不能为更大的系统提供可见性。
画到图上可能会像下面这样,不过上面的这些点表示为一个范围可能更好:
这让我思考,右上角的部分会是什么样子。一个能让我们洞察细节的、有用的可视化该是什么样子?有没有一种方法可以在较低的层次上,将整个系统的路径可视化?
看图的感觉仍然像看地图。在那一刻,你把在代码中看到的东西在空间上转化为图表,就像你在一个不熟悉的地方用地图确定方向。就像电脑上的东西,我们用了桌面隐喻一样,我想知道是否有另一种方式将代码可视化为实际存在的东西,以便让翻译过程变得更容易。
如果我们用“玩具型可视化(toy visualization)”在物理空间中表示代码,会怎么样?
看下面这个基本的例子,我们有一个 Counter 类,它有一个私有的 count_ 变量并提供了各种访问方法。也许它会在一个简单的 main 函数中被访问,做一些计数。
#include <IOStream>
class Counter {
public:
void reset() {
count_ = 0;
}
int get() {
return count_;
}
void incr() {
++count_;
}
void set_count(int x) {
count_ = x;
}
private:
int count_;
};
int main() {
Counter counter;
counter.reset();
counter.incr();
counter.incr();
std::cout << counter.get();
}
如果我们把代码的不同部分表示为三维结构,会怎么样?如果我们把 Counter 类表示为一个大房间,墙上写着咒语,会怎么样?你可以想象代码的可能路径,变量和它们所代表的事物之间的联系,就像下图中的粉红线:
我们可以从中看到什么?首先,作为私有变量,count_ 的用法显然没有什么不当,因为没有任何粉红线从它那里离开房间。公共方法也很清楚,它们与房间外的事物有粉红色的连接。
更有趣的是,这个房间与另一个房间相连,会是什么样子?main 函数可以是另一个房间,其符咒线也做了相应的连接:
现在,我们可以跨房间对话了;在 main 函数调用 counter.reset() 的地方,我们可以有一个从调用者 main 到被调用者 Counter 类的连接。你甚至可以想象有一个调试器单步遍历这个过程,观察这条线路上的参数和返回值。想象一下,我们可以放大不同的区域来查看本地状态和数值,然后沿着调用路径返回到活动区域。
这有用吗?
像这样的东西有用吗?我不确定。用这样的方式走查一个熟悉的代码库会很有趣,特别是当你能在 3D 表示(或是 VR 环境)中做空间探索,并可以根据需要缩小和放大时。当第一次探索一个新的代码库,查看事物之间的连接关系时,不知道它是否会特别有用。不过,我确实处理过一些代码库,如果这样看会非常吓人。我很想看看这样看会有什么不同,每新引入一个类,这儿那儿就会引入新的连接。
话虽如此,我认为在制作这样的东西时,你至少会遇到以下问题:
问题
有几个考量因素使这个问题变得棘手。一个是物理位置的变化比代码的变化耗时通常长得多。使用熟悉的物理位置作为记忆宫殿,一部分原因是它在你的记忆中每次出现都是一样的。如果你在记忆一副扑克牌,你可以把梅花 A 暂时存放在橱柜门后面,下次你需要存放扑克牌时,橱柜门仍然在那。
如果你的代码库经常变化,反映事物空间布局的地图就可能会发生变化,不管这些地图是 3D 生成的还是纯意识的。这就像回到一个你曾经熟悉的地方,想象一下,不只是地标变了,路也改道了。即使我们天生具有记忆空间事物的天赋,如果那个空间发生了变化,如果我们不得不重新学习,我们还能从空间可视化受益吗?
看看这样的东西对于探索一个新代码库(就像使用地图探索一座新的城市),以及随着时间推移再次回到该代码库(就像离开很长时间后回到自己的家乡),有多大帮助,这会很有趣。
我对这一领域的数据可视化不是很熟悉(其他领域的也不熟悉),但经过简单的搜索(也就是 30 分钟的 Google 搜索),我发现有几个项目似乎在做类似的事情:
下面是读者指出的一些项目:
如果你了解到其他类似的项目,欢迎和我联系,我非常乐意听到更多这样的项目!
显然,这个概念并不是什么突破性的东西,但我认为,对于我们使用的工具,这是一个有趣的思考方式,重要的是,我们如何做得更好。一定有更好的方法存在,设想下它们可能的样子会很有趣。