代码阅读的姿势

面向对象编程

众里寻他千百度,蓦然回首,那人却在,灯火阑珊处。

一般地,在一个程序员的日常工作之中,绝大多数时间都是在「阅读代码」,而不是在「写代码」。但是,阅读代码往往是一件很枯燥的事情,尤其当遇到了一个不漂亮的设计,反抗的心理往往更加强烈。

事实上,变换一下习惯、思路和方法,代码阅读其实是一个很享受的过程。阅读代码的模式,实践和习惯,集大成者莫过于希腊作者Diomidis Spinellis的经典之作:Code Reading, The Open Source Perspective.。本文从另外一个视角出发,谈谈我自己阅读代码的一些习惯,期待找到更多知音的共鸣。

工欲善其事,必先利其器

首先,阅读代码之前先准备好一个称心如意的工具箱,包括IDE, UMLMind Maping等工具。我主要使用的编程语言包括C++, Scala, Java, Ruby;对于Scala, Java, Ruby编程,我更偏向使用JetBrain公司的产品;而对于C++编程,我依然还在使用Eclipse,因为Clion的特性还没有让我满意。

其次,高效地使用快捷键,这是一个良好的代码阅读习惯,它极大地提高了代码阅读的效率和质量。例如,查看类层次关系,函数调用链,方法引用点等等。

拔掉鼠标,减低对鼠标的依赖。当发现没有鼠标而导致工作无法进行下去时,尝试寻找对应的快捷键。通过日常的点滴积累,工作效率必然能够得到成倍的提高。

力行而后知之真

阅读代码一种常见的反模式就是「通过Debug的方式来阅读代码」。作者不推荐这种代码阅读的方式,其一,因为运行时线程间的切换很容易导致方向的迷失;其二,了解代码调用栈对于理解系统行为并非见得有效,因为其包含太多实现细节,不易发现问题的本质。

但在阅读代码之前,有几件事情是必须做的。其一,手动地构建一次工程,并运行测试用例;其二,亲自动手写几个Demo感受一下。

先将工程跑起来,目的不是为了Debug代码,而是在于了解工程构建的方式,及其认识系统的基本结构,并体会系统的使用方式。

如果条件允许,可以尝试使用ATDD的方式,发现和挖掘系统的行为。通过这个过程,将自己当成一个客户,思考系统的行为,这是理解系统最重要的基石。

发现领域模型

发现「领域模型」是阅读代码最重要的一个目标,因为领域模型是系统的灵魂所在。通过代码阅读,找到系统本质的模型,并通过自己的模式表达出来,你才能真正地Hold住了系统,否则一切都是空谈。

首要的任务,就是找到系统的边界,并能够以「抽象的思维」思考外部系统的行为特征。其次,寻找系统潜在的,并能表达系统的重要概念,及其它们之间的关联关系。

细节是魔鬼

纠结于细节,将导致代码阅读代码的效率和质量大大折扣。例如,日志打印,解决Bug的补丁实现,某版本分支的兼容方案,某变态用户需求的锤子代码等等。

阅读代码的一个常见的反模式就是「给代码做批注」。这是一个高耗低效,投入产出比极低的实践。越是优雅的系统,注释越少;越是复杂的系统,再多的注释也是于事无补。

我有一个代码阅读的习惯,为代码阅读建立一个单独的code-reading分支,一边阅读代码,一边删除这些无关的代码。

$ git checkout -b code-reading

删除这些噪声后,你会发现系统根本没有想象之中那么复杂。事实上,系统的复杂性,往往都是之前不成熟的设计和实现导致的额外复杂度。

适可而止

阅读代码的一个常见的反模式就是「一根筋走到底,不到黄河绝不死心」。程序员都拥有一颗好奇心,总是对不清楚的事情感兴趣。例如,消息是怎么发送出去的?任务调度工作原理是什么?数据存储怎么做到的等等;虽然这种勇气值得赞扬,但在代码阅读时绝对不值得鼓励。

还有另外一个常见的反模式就是「追踪函数调用栈」。这是一个极度枯燥的过程,常常导致思维的僵化;因为你永远活在作者的阴影下,完全没有自我。

我个人阅读代码的时候,函数调用栈深度绝不超过3,然后使用抽象的思维方式思考底层的调用。因为我发现,随着年龄的增长,曾今值得骄傲的记忆力,现在逐渐地变成自己的短板。当我尝试追踪过深的调用栈之后,之前的阅读信息完全地消失记忆了。

也就是说,我更习惯于「广度遍历」,而不习惯于「深度遍历」的阅读方式。这样,我才能找到系统隐晦存在的「分层概念」,并理顺系统的结构。

发现她的美

三人行,必有我师焉。在代码阅读代码时,当发现好的设计,包括实现模式,习惯用法等,千万不要错过;否则过上一段时间,这次代码阅读对你来说就没有什么价值了。

当我发现一个好的设计时,我会尝试使用类图,状态机,时序图等方式来表达设计;如果发现潜在的不足,将自己的想法补充进去,将更加完美。

例如,当我阅读Hamcrest时,尝试画画类图,并体会它们之间关系,感受一下设计的美感,也是受益颇多的。

Hamcrest匹配器

尝试重构

因为这是一次代码阅读的过程,不会因为重构带来潜在风险的问题。在一些复杂的逻辑,通过重构的等价变换可以将其变得更加明晰,直观。

对于一个巨函数,我常常会提取出一个抽象的代码层次,以便发现它潜在的本质逻辑。例如,这是一个ArrayBuffer的实现,当需要在尾部添加一个元素时,既有的设计是这样子的。

def +=(elem: A): this.type = {
  if (size + 1 > array.length) {
    var newSize: Long = array.length
    while (n > newSize)
      newSize *= 2
    newSize = math.min(newSize, Int.MaxValue).toInt
  
    val newArray = new Array[AnyRef](newSize)
    System.arraycopy(array, 0, newArray, 0, size)
    array = newArray
  }
  array(size) = elem.asInstanceOf[AnyRef]
  size += 1
  this
}

这段代码给阅读造成了极大的障碍,我会通过快速的函数提取,发现逻辑的主干。

def +=(elem: A): this.type = {
  if (atCapacity)
    grow()
  addElement(elem)
}

至于atCapacity, grow, addElement是怎么实现的,压根不用关心,因为我已经达到阅读代码的效果了。

形式化

当阅读代码时,有部分人习惯画程序的「流程图」。相反,我几乎从来不会画「流程图」,因为流程图反映了太多的实现细节,而不能深刻地反映算法的本质。

我更倾向于使用「形式化」的方式来描述问题。它拥有数学的美感,简洁的表达方式,及其高度抽象的思维,对挖掘问题本质极其关键。

例如,对于FizzBuzzWhizz的问题,相对于冗长的文字描述,流程图等方式,形式化的方式将更加简单,并富有表达力。

3, 5, 7为输入,形式化后描述后,可清晰地挖掘出问题的本质所在。

r1: times(3) => Fizz || 
    times(5) => Buzz ||
    times(7) => Whizz

r2: times(3) && times(5) && times(7) => FizzBuzzWhizz ||
    times(3) && times(5) => FizzBuzz  ||
    times(3) && times(7) => FizzWhizz ||
    times(5) && times(7) => BuzzWhizz

r3: contains(3) => Fizz

rd: others => string of others

spec: r3 || r2 || r1 || rd

实例化

实例化是认识问题的一种重要方法,当逻辑非常复杂时,一个简单例子往往使自己豁然开朗。在理想的情况下,实例化可以做成自动化的测试用例,并以此描述系统的行为。

如果存在某个算法和实现都相当复杂时,也可以通过实例化探究算法的工作原理,这对于理解问题本身大有益处。

Spark中划分DAG算法为例。假设GFinalRDD,从后往前按照RDD的依赖关系,依次识别出各个Stage的起始边界。

Stage划分算法
  • Stage 3的划分:

    1. GB之间是Narrow Dependency,规约为同一Stage(3);
    2. BA之间是Wide DependencyA为新的FinalRDD,递归调用此过程;
    3. GF之间是Wide DependencyF为新的FinalRDD,递归调用此过程;
  • Stage 1的划分

    1. A没有父亲RDDStage(1)划分结束。特殊地Stage(1)仅包含RDD A
  • Stage 2的划分:

    1. RDD之间的关系都为Narrow Dependency,规约为同一个Stage(2);
    2. 直至RDD C, E,因没有父亲RDDStage(2)划分结束;

最终,形成了Stage的依赖关系,依次提交Stage(TaskSet)TaskScheduler进行调度执行。

独乐乐不如众乐乐

与他人分享你的经验,也许可以找到更多的启发;尤其对于熟知该领域的人沟通,如果是Owner就更好了,更能得到意外的惊喜和收获。

也可以通过各种渠道,收集他人的经验,并结合自己的思考,推敲出自己的理解,如此才能将知识放入自己的囊中。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 211,948评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,371评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,490评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,521评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,627评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,842评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,997评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,741评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,203评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,534评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,673评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,339评论 4 330
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,955评论 3 313
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,770评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,000评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,394评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,562评论 2 349

推荐阅读更多精彩内容