重构 代码阅读的姿势

horance · December 31, 2020 · Last by strongant replied at January 10, 2021 · 66 hits
Topic has been selected as the excellent topic by the admin.

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

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

事实上,变换一下习惯、思路和方法,代码阅读其实是一个很享受的过程。阅读代码的模式,实践和习惯,集大成者莫过于希腊作者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时,尝试画画类图,并体会它们之间关系,感受一下设计的美感,也是受益颇多的。

尝试重构

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

对于一个巨函数,我常常会提取出一个抽象的代码层次,以便发现它潜在的本质逻辑。例如,这是一个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 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就更好了,更能得到意外的惊喜和收获。

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

「软件匠艺社区」旨在传播匠艺精神,通过分享好的「工作方式」,让帮助程序员更加快乐高效地编程!

admin mark as excellent topic. 04 Jan 00:39
You need to Sign in before reply, if you don't have an account, please Sign up first.