我相信现在大学中所进行的计算机科学(CS)教育缺乏对学生关于如何在非常大的代码库中查找信息(navigate)以及如何快速地理解代码库的指导。
可能大多数计算机专业(CS)的学生最后会到中小型的企业工作,在这样的企业中,他们往往只需要关注某一个规模不大的程序或代码库。对于这样的代码库,公司的老员工将非常熟悉,所以能够在较高的层面上(at a high level)为新人对其中的每个部分都解释清楚。
另一个可能的原因(很可能是更准确的,但是大学教授们不会承认的一个原因)是老师们自己都从来没有处理过超过十万行的代码库,更是很少处理别人写的代码(除了他们自己的学生写的),但这是另外一篇文章讨论的内容。
回到主题上来,我认为当我们加入了像微软、亚马逊、谷歌、Facebook或者任何那些为数不多的可以真正被称作“大型技术公司”的企业中时,很容易就陷入迷茫(get lost)并缺乏生产力(fail to be productive)。
从我以前指导过的实习生的实际情况来看,似乎没有人教过他们当代码像消防管出来的水一样向他们涌来时该如何应对。所以我这里出一份力,希望能够帮助到未来的程序员们。
在许多情况下,你可能会发现自己必须浏览一个大的外部代码库。其中一些例子如下:
你团队中最资深的人员可能仅待了不到一年,特别是在一个流失率很高的公司,他们自己可能不知道团队要维护的大部分系统,因为他们目前还不需要处理该系统。现在,一个新功能需要添加到遗留系统中,猜猜老板选择怎么做?
你正在处理另一个团队的代码,他们没有时间(或不想)向你解释,但你又想/需要了解该代码。
你正在从事一些跨部门的工作,例如性能或效率,这要求你不断在其他人的代码中工作,并以每周(或每天)为基础进行对比。
与长时间使用相同的代码相比,这通常会使你处于劣势。
那么,在不浪费大量时间的前提下,如何才能简单地地找到相关的代码和编写(理解)这个相同的代码呢?
下面会告诉你。
每个拥有优良品行的幸存者都会告诉你:野外生存的第一条规则是“不要慌张”。
那么如果在上午2点,服务器正在处于崩库边缘,你的钱像血一样流失,所有的高级工程师都在 Tahoe 进行一年一度的公司旅行中,你被迫忙于技术支持,你该怎么做呢?
现在不是听之任之,并希冀你已经辍学,在和怀俄明州的嬉皮姐妹一起养山羊的时候。 该死的山羊! 这里有很多待读的代码。
你需要擅长的第一件事是快速找到正确的代码段,并将其放在你的眼球前。这可能听起来很愚蠢,但是我已经浪费了很多时间(有时会更多)阅读那些我不感兴趣的代码片段。
在花费大量时间阅读部分大型程序之前,需要做两件事情:首先要找到相关的代码,然后在深入阅读前再次确定这就是正确代码段。
想要弄清楚哪些代码引起了一些行为的发生或者某些UI元素的出现,一个最简单确实最强大的方法,就是检索(grep for,指的应该是使用类似 grep 的行搜索工具进行搜索)你能找到的一些奇怪的词句(odd phrases)。
如果你不知道如何在一个代码库中检索信息,我建议你去查一下资料。如果你用的是git,那么这里有一篇 很好的文章 可以给你这方面的指引。如果你不会正则表达式,那你一定要学会它。因为使用正则表达式是一个非常有用的技能,而且网上有大量很好的教程。我好像离题了...
如果一个 web 页面上出现了这样一句话“All events are logged in the Pacific (PST) timezone.”,我认为那是一条很有价值的线索。你想一下,在一个有两百万行代码的库中这样一个带着特定大小写字符的长句会出现多少次?可能有那么几次,但是至少可以把我们的目标锁定在为数不多的几个文件中。接下来我们再根据找到的文件的文件名来进一步过滤掉明显无关的代码。
“CatShouldBeADogException thrown”之类的信息是另外一个很有价值的线索。 自定义的异常(custom exception)(特别是在Java中)通常只会在4到5个地方被用到。 再结合一些你正在处理的环境中的信息, 例如 stack trace 中的其他内容,或者在这之前的两条告诉你程序进入了某个方法的日志记录, 你可以很快地缩小范围并找到那几行相关的代码。
日志行消息是查找代码的另一个好方法。“Starting to process log lines with x lines” 是一个不太可取的短语搜索。 首先,x是动态的,意思是你真正应该 grep 的是 “Starting to process log lines”。 因为语句可能包含引号和打印语句,所以更多的 grep 将是无意义的。
通常这些方法是一个好的开始。但是如果这都不能让你找到你想要的信息呢?
你知道谁在开发某个特性,但你知道他们正在编写哪部分代码吗?很简单,只需要对他们进行责任追踪即可!
不过什么是责任追踪?
任何时候某人提交了一段代码到某个源代码版本控制库(git、mercurial、svn 等),他们的名字或登录名都会与这次提交关联起来。
如果你知道某人正在处理某个特定的功能,那么查找相关的代码就非常简单,就像在库中寻找他们接触过的文件一样简单!
如果你对别人正在做什么感到好奇,或者,你认为他们就是奇才,想看看大师级的示例(嘿,这是学习的好方法),那么这个方法很适合你。
具体步骤取决于源码版本控制工具,举例来说,如果你想看到我提交的所有东西(包括修改的文件),Git 中可以使用 git log —name-only —author=imperio59。
从“代码入口”开始
有时候,并没有什么好的方式可以知道从哪里开始看起。你已经尝试过全文搜索定位一些生僻的词语,你也尝试过单步调试跟踪代码。现在你无计可施了。现在也许是时候重整旗鼓、从最开头的代码开始了。
寻找当前程序代码的入口点(从多种方面来看,这都是具有指导性意义的),顺着代码的走向前进。不过,这种方法可能是相当耗时的,所以请把它当成最后一招。
好了,你找到了你想阅读的相关代码了。那么接下来怎么办呢?
阅读的时间到了!但不只是随便看看,不行的。我们要阅读,所以我们可以知道我们正在阅读什么。这是有差别的看。愚蠢地逐行扫描对我们没有任何好处,不是为了对我们待读的代码一知半解,希望我们能够获得一些神奇的理解,或者通过深入、重复的阅读,我们将会“get it”。
在我看来,有三个主要的东西可以阻碍你对别人的代码的理解:
你不明白代码编写的一些规约。 (命名约定,样式约定...)
你不明白特定函数究竟做了什么,因为你从来没有遇到过。
你不明白编程语言本身的一些功能。
我们来看看如何处理这些问题:
在对变量、类或方法命名时,人们并不会使用那么多的约定。有赖于你所使用的语言,一些命名约定可能就是语言本身的一部分(变量或函数应该以此样式命名,因为它就得是这样)。
对于命名约定,我发现人们通常会坚持基础的常识。历久弥新的匈牙利命名法或者是其一小部分现如今仍然在被广泛地运用着。有时候,人们会向类的每一个成员变量名字前头添加一个小写字母m,以此区别于局部变量。有些人就干脆规定成员变量要使用首字母小写的驼峰命名法来写,而本地变量则全部小写(memberVariablesInCamelCase ),并且单词之间用下划线分割(local_variables_have_underscores)。有时私有函数名以下划线开头(_startWithAnUnderscore),而公共方法则并不这样做(publicMethodsDont),同样是以示区分。 不管约定是如何定义的,都得是清晰并且能快速进行识别的。如果幸运的话,内部知识库里面会有一个公开发行的代码风格指南,这样大家就省事儿了。关键是你得去阅读它。回头读过代码之后再来读一下风格指南。 代码约定不是魔鬼,它们通过帮助你快速地对变量和函数的类型进行归类,来帮助你加快阅读和理解代码的加速。不过只有在你对约定相当熟悉的前提下,他们才会有加快速度的效果,不这样的话,它们就会在你每次遇到它时通过增加一些额外的沟沟坎坎来弥补你对此的理解。 (“为什么他们现在命名为 mServiceManager 而不是 serviceManager 呢?”)。 最后就是,有时人们会发明自己的风格指南,但并不会在任何地方发表。如果照我的经历来说,要么就是编写代码的人是当时比较匆忙,回头没有写一个风格指南,或者留下任何的记录文件(以防留下了什么蛋疼的BUG到时候没法追溯源头),要么就是这个人太自我——这种人经常对写一个风格指南或者说明一下他们在做什么表示不屑(诸如“我的代码不需要单元测试,因为它没有错误”此类的,对吧!),在这样的情况下,代码会被过度设计,封装得跟一个铅球似的,而且BUG重重。 无论采用哪种方式,使用一些非标准的,不成文的编码约定就是一个很可能会有问题发生的迹象,这时候就当心点吧。
下一个事情是,当你阅读的代码是你从来没有听说过的功能时会减慢你的阅读速度。 如果你退一步,看看这个语言,它是由你能理解的简单的代码块组成的:创建新的变量,分配它们的值,做一些条件逻辑,也许是一些循环,如果你运气很糟,它可能是一些二进制算法。但是假设你的语言学的很好,接下来要知道的是你阅读的功能是做什么的。 为了本文的目的,我们将它们分为两种类型的函数: 1、公共函数(也称为库函数) 2、“业务逻辑”函数。 公共函数或库函数是一类几乎可复用的函数。需要格式化日期?这有一个公共函数可用。解析一些JSON?这还有另一个可用的。 通常这些方法是语言库的标准集合的一部分。有时,他们是某些第三方库的一部分(有开源的,有不开源的)。
如果你从未看到过其中一个特定的内容,那么阅读其文档是很有用的。它需要什么样的输入?它产生什么样的输出?它是否改变输入还是仅使用它?调用函数是否有任何副作用?函数执行完成有多慢/多快? Utility函数的另一个子类是专门针对你所在代码库的库函数。这些是你正在阅读的代码中每隔五行都会出现的函数,并且似乎被用于所有内容。再强调一次,花点时间了解它是明智的。 接下来是业务逻辑函数。这些就是你代码“土豆”中的肉。他们确实是值得你在此认真阅读的部分。 通读他们但不是逐行。除非你确实需要。 让我来说明下原因。 每当我觉得我需要阅读我正在分析的程序中一部分的一个函数时,我有两个选择。我可以选择深入了解它的每个部分,或者只是选择大概了解。 大概了解意味着我将知道函数的名称,输入变量的名称,速读代码以获得对其功能的高层理解,找出它返回值以及它对程序的其他部分所执行的操作(或全部,在我心里这都是代码的味道),然后继续阅读其他的。 除非我在重构影响这个函数的部分,否则我不会逐行阅读。我不需要这么做!一旦我知道了它的作用,BOOM,我就继续读下一个! 每天有太少的时间让我知道这15000行代码究竟什么功能。我只是想知道标题,了解这本书的框架,并不是阅读完整的故事。 这里有一个例外,那就是由monkey(有证据表明是他们编写的)编写的代码。 如果每个变量都被命名为“thingA,thingB,thingC”,或者比这个更糟的,一半的方法在在一个名为“util”的文件夹中,那么你将为此大动干戈了。这些人通常不知道如何专业地开始编写代码,或者如何组织很多代码,并且起初可能不会被雇用。
在你正确地命名代码时,大多数时候,代码是自带文档的(代码即文档)。如果你不正确地命名,这将是一团乱麻。
这也许是时候更新你自己的简历,并给那些你在 LinkedIn 上收到的招聘人员的邮件回信了。如果你维护的代码超过 30% 是 monkey code(随机代码),那么为了维护这些代码你的薪酬可能有点低。(毕竟你正在阅读这篇文章,这意味着你关心软件和你的工作。同样的情况不能说是为了编写这段代码的人,这代码让你调试到凌晨两点,而他正在睡得正香)。
最后,单元测试非常有助于编写代码。
如果你发现你正在阅读的函数有相应的单元测试代码,通过阅读测试代码可能会更快地了解该函数的功能。
当然,前提是测试代码不是由猴子编写的。
我的天呐!什么?你应该为自己感到羞耻!
或者也不需要。
我的意思是开始吧。然后呢?你不知道,所以你要去查找。不要觉得自己很糟糕,可以自我训练一下。
你觉得最好的程序员怎么会这么优秀呢?其实他们只是阅读大量相关的软件和代码,一遍又一遍,直到这成为一种习惯。
所以你从来没有在 Java 中使用过 volatile 关键字,但突然间每一个其他的文件都有一个声明为 volatile 的成员变量?那么这就是去阅读 Java 并发和线程文档的很好的时机,以获得更多的知识。慢慢的,对它的使用将会变得得心应手。
你也从来没有见过有人在 Javascript 中使用绑定参数(如 foo.bind(this, arg1) )?那么这可能是时候更多地了解 JS 闭包了。
事实上,如果你认为在你毕业的那一刻学习就已经结束了,那么一定是某人欺骗了你(或是你自己欺骗自己)。学校(或任何形式你所获得的编程相关的初等教育)给了你基础。其余你需要学习的取决于你自己、取决于你专攻的方向,以及日常工作中遇到的问题。
另外,科技产业也在不断地发展、创新和重塑自身。如今的软件不像十年前那样编写了,甚至与五年前的也不一样。
不要对自己不知道的东西感到沮丧。学会去查找,这会让你变得更聪明,并提升自己的技能。这也是长久发展的唯一路径。
所以你现在应该已经知道如何在一个大的外国代码库中找到相关的代码,希望你已经学到了一些如何快速了解代码库代码而不被难住的知识。 最后,我想说最后一件事:成为一个优秀的代码公民。 有时代码可以像大城市一样。 它的一些部分是美好而干净的。 一些部分只剩被胶带粘住的窗户。 尽可能地改善处于糟糕状态的地方,尽管严格的说这部分不关你事儿(但是在检查并修补之前,请先获得所有者的许可或代码审查)。 毕竟,你永远都不会知道,这可能是一段每天早上2点给你写信的代码。 当它工作时,你会很高兴你是重构它的人。
