zoukankan      html  css  js  c++  java
  • 阅读代码的姿势

    作为程序员坚持阅读代码是持续自我提升的有效方法之一。有心的程序员不仅要找到优秀代码阅读,更要注意阅读的方法,从整体架构掌握再逐步深入细节,先“广度优先”,再选自己感兴趣的方面进行“深度历险”。

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

    事实上,变换一下习惯、思路和方法,代码阅读其实是一个很享受的过程。阅读代码的模式,实践和习惯,集大成者莫过于希腊作者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就更好了,更能得到意外的惊喜和收获。

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

    原文链接:http://www.jianshu.com/p/3e6d4c520719

  • 相关阅读:
    Oracle 查看表空间的使用情况SQL语句
    汇总查询
    conky配置2
    数据库更新
    weka简介和回归转自chinakdd
    子查询
    ubuntu常用命令
    查询
    数据库中的连接
    测试用的数据库表及其数据
  • 原文地址:https://www.cnblogs.com/doit8791/p/5797656.html
Copyright © 2011-2022 走看看