• 程序猿的凝视之道


    SAP高级开发project师 范德成

    2014年10月25日

    写这篇文章之前,我所思考的前一个问题是代码的质量。而在编写了好的代码的前提下。代码的凝视就成了代码质量的还有一部分——它的作用初看时显得并不那么大,可是越到后面越显得重要。

    当一名勤奋的程序猿为了一个大项目,洋洋洒洒地写了数千行代码之后,他转而去做该项目的还有一个模块。等到一年后,他回头再来看他之前写的这几千行代码时。假设没有具体有意义的凝视,那就得挠头了——由于当初没有写凝视。

    为什么会出现这样的情况呢?我们当今所使用的编程语言,比方Java、C#、python等,不是早就是高级语言了嘛?已经不仅仅是给机器看的代码,这些代码也能让人读懂了嘛?的确,它们是高级语言。但问题却在于,代码本身仅仅写了怎么做一件事。做了这件事的结果怎样。

    至于它做的是什么事,为什么要做这件事,在什么情况下能够用它来做这件事,代码本身是体现不出来的。

    因此。凝视有其独特的用处。

    代码本身的质量,来自正确性、安全性、可用性、可读性、可维护性、效率等好几个方面。好的凝视。则可以帮助代码提升其可读性和可维护性。并终于为正确性等其它几个方带来正面影响。

    那么,在保证代码本身的高质量的前提下,凝视应该怎么写。才干有效呢?下面是我个人在多年工作中总结出来的经验教训。它适用于绝大多数命令式(imperative)编程语言:

    1. 要为复杂的函数接口写清晰的凝视。
    2. 凝视中要写清楚重要的细节。
    3. 凝视本身不要有冗余信息。
    4. 凝视要随时更新。
    5. 当遇到复杂的、不直观的实现时。也要为实现写凝视。


    6. 要为简化、抽象和缩写的变量名或函数名,凝视其全称及其含义。
    7. 不要为不言自明的代码加凝视。
    8. 不要为频繁变化的代码写冗余的凝视。

    第一点。要为复杂的函数接口写清晰的凝视。这里的函数接口指的是函数名及其參数、返回值、异常等的规范。更严格地来说。关于函数接口的凝视定义了一个函数的契约。尽管我们所使用的编程语言不一定支持面向契约的编程,或者我们不选择这样一种编程模式,但我们仍能够在概念上用凝视来表示一个函数的契约。

    函数接口的凝视该怎么写呢?以C#为例,它的一个函数会有函数名,參数和返回值。

    同一时候,參数和返回值又分别有其类型。还会有潜在的异常抛出。例如以下,是一个假想的做归并排序的函数的接口(该接口明显过于复杂,但其目的是为了演示怎样写凝视):

    bool MergeSort<T>(T[] array, int begin, int len, IComparer<T> comparer)
    {
    }

    C#语言支持XML凝视,其功能类似于Javadoc。

    并且,它的XML凝视正好支持我们要凝视的契约的全部内容。因此,我们能够用XML凝视来写。

    对于这个函数,我们的凝视例如以下:

    /// <summary>
    /// Performs merge sort on a part of an array with a comparer.
    /// </summary>
    /// <typeparam name="T">the type of members in the array to be sorted</param>
    /// <param name="array">the array to sort</param>
    /// <param name="begin">the beginning index of the part in the array to be sorted</param>
    /// <param name="len">the length of the part in the array to be sorted</param>
    /// <param name="comparer">the comparer used to compare elements in the array; see documentation on <see cref="System.Collections.Generic.IComparer<T>">IComparer</see> for more information</param>
    /// <returns>
    /// Whether the part of the array is already sorted.
    /// </returns>
    /// <remarks>
    /// <para>This method checks whether the specified part of the source array is already sorted. If it is already sorted, the method returns true directly without changing the array. Otherwise, it sorts the part and returns false.</para>
    /// <para>This method performs stable sort on the specified part of the array.</para>
    /// <para>After calling this method, the range of the array from position <paramref name="begin" /> and of length <paramref name="len" /> is sorted.</para>
    /// <para>The time complexity of this method is O(n log n), where n is the length of the part being sorted.</para>
    /// </remarks>
    /// <exception cref="System.IndexOutOfRangeException">
    /// An IndexOutOfRangeException exception is thrown if the indices <paramref name="begin" /> and <paramref name="len" /> are out of range.
    /// </exception>
    bool MergeSort<T>(T[] array, int begin, int len, IComparer<T> comparer)

    中文翻译例如以下:

    /// <summary>
    /// 利用一个比較器,对一个数组中的一段内容运行归并排序。
    /// </summary>
    /// <typeparam name="T">被排序数组的元素类型</param>
    /// <param name="array">要排序的数组</param>
    /// <param name="begin">数组中要排序的段的起始下标</param>
    /// <param name="len">数组中要排序的段的长度</param>
    /// <param name="comparer">用于比較数组中元素大小的比較器。具体信息请參见<see cref="System.Collections.Generic.IComparer<T>">IComparer</see>的文档。</param>
    /// <returns>
    /// 该数组段是否本来就是有序的。
    /// </returns>
    /// <remarks>
    /// <para>本方法将检查源数组的指定段是否已经处于有序状态。

    假设是这样,本方法将不改动数组内容。直接返回true。

    否则。本方法对指定段进行排序并返回false。

    </para> /// <para>本方法对数组的指定段运行的排序是稳定排序。</para> /// <para>在调用本方法之后。数组中从<paramref name="begin" />開始。长度为<paramref name="len" />的段将被排序。</para> /// <para>本函数的时间复杂度为O(n log n),当中n是被排序部分的长度。</para> /// </remarks> /// <exception cref="System.IndexOutOfRangeException"> /// 若下标<paramref name="begin" />和<paramref name="len" />的范围溢出了,则抛出一个IndexOutOfRangeException异常。 /// </exception> bool MergeSort<T>(T[] array, int begin, int len, IComparer<T> comparer)

    C#的XML凝视还支持很多其它标签,比方example(演示样例)等。可是在我们日常的编程过程中,这些标签仅仅是依据须要,偶尔用到。而上面我讲的这些则是常常要用到的。

    我们来看一下。

    首先,对于该函数。我们有一个简单介绍(见summary部分)。然后。对于每个參数,以及函数的返回值。我们都要作一下说明。典型的异常要做说明,但并不是每个都有必要。对于复杂的函数。在简单介绍里面没有办法用一句话概括全部意思的,须要写一段注解(remarks部分)。

    当中函数的简单介绍,力求用一句话(最多两句)把该函数该做什么事情给讲清楚。

    參数和返回值的凝视,要把它们的含义讲一下,把它们的特殊值讲一下。特殊值的样例就是和平时传的值有所差别的值。

    比方,对于某些可选參数。传入null表示忽略该參数。那么这种值就是特殊值,须要得到说明。

    注解部分则要增加一些函数契约的细节。

    比方,前置条件(函数调用之前须要满足的条件)、后置条件(函数调用后,数据会变成什么样。比方这里的已排序状态就是后置条件)、时空复杂度(假设有需求的话)、典型的应用场合、特殊的应用场合(这对于一些须要在特定上下文中运行的业务API来说非常重要)等等。

    这里,我想特别说明一下对于异常的凝视。

    各个语言中。对于异常在语法上有着不同要求。C#不支持checked exception,它的设计者Anders Hejlsberg也不建议我们使用checked exception;Java则要求除了程序bug以外的异常都作为checked exception。所谓checked exception。是这种一些异常类型,当它们被当前函数抛出时,当前函数必须在原型(即函数的接口)中声明这些异常。这样做的优点是,调用方知道将会收到哪些异常。缺点则是,应用程序扩展起来非常不方便:当须要从底层添加一个新的异常类时,要么就得在应用程序函数体内调用这些API的地方捕获这些异常,要么就得在应用程序的函数原型中声明这些异常。否则就会导致编译错误。这对于一些库来说,就要求它们为了应用程序着想。把它们的全部异常类从一个基类衍生出来,从而应用程序仅仅须要声明那个基类就可以。出于这个原因,我们写凝视。就仅仅为典型的异常(在实际场合中easy遇到的异常)写凝视。那些非常难出现,甚至理论上不可能出现的异常全然不用写。并且,必要的时候,尽管checked exception声明的可能是基类,但我们的凝视却要反映出子类异常的详细发生情况。

    第二点,凝视中要写清楚重要的细节。在上一点的样例中已经带到过这一点:排序算法的时间复杂度和稳定性就是这样的重要细节。

    第三点,凝视本身不要有冗余信息。

    凝视是用来解释程序的。

    写凝视的时候,为了凝视的完整性。其内容可能会和程序代码的含义有一些重叠,亦即冗余信息。这一点非常难全然避免。

    可是。凝视内部不同部分之间出现的冗余则能够被避免。

    要避免这样的冗余。没有定法,主要就是在写凝视的过程中多读几遍,把反复内容删掉;有些地方能够用引用的方式。避免写反复的凝视。

    第四点。凝视要随时更新。这一点是为了拥有高质量的凝视。我们所必须做的事情。每次函数期望的功能、接口、契约发生改变时,凝视也应该被对应地更新。

    当然,在实际的软件project中,全然理想的时刻更新非常难保证,但至少要力求对于大的变化。凝视是足够新的,并且。我们在阅读程序代码的过程中假设发现凝视不对。也能够顺便调研一下程序的行为。并据此来更新凝视。

    第五点。当遇到复杂的、不直观的实现时,也要为实现写凝视。

    有的时候。一个函数是非常简短的,它的实现不言自明。但也有时候。一个函数的实现比較复杂。这可能是因为复杂的算法、复杂的业务逻辑等原因,这时,用以表示运行步骤的凝视就会非常方便。以下的拓扑排序函数就演示了这一点:

    def topological_sort(graph, output_func):
        # Time complexity is O(n ^ 2)
        while len(graph) > 0:
            # Output dependency-free nodes
            to_pop_node_name = []
            for node_name in graph:
                # Remove and output all nodes without dependencies
                if len(graph[node_name].dependencies) == 0:
                    output_func(node_name)
                    to_pop_node_name.append(node_name)
            # Remove the nodes
            for node_name in to_pop_node_name:
                graph.pop(node_name)
            to_pop_node_name = None # finished using
            # Remove dependency links
            for node_name in graph:
                current_node = graph[node_name]
                to_pop_node_name = []
                for child_node_name in current_node.dependencies:
                    if child_node_name not in graph:
                        to_pop_node_name.append(child_node_name)
                for child_node_name in to_pop_node_name:
                    current_node.dependencies.pop(child_node_name)
                to_pop_node_name = None # finished using

    当中,Output dependency-free nodes、Remove the nodes等都是对一个代码块的凝视。此类凝视能表示出程序的步骤。这样的凝视还能够跨越更大的范围。此时的技巧是。用大括号或begin、end的字样来表明其范围。例如以下所看到的:

    int i;
    int max = -1;
    int sum = 0;
    
    // Do the first thing {
    for (i = 0; i < arr.Length; i++) {
        if (max < 0 || arr[i] > max) {
            max = arr[i];
        }
    }
    // }
    // Do the second thing {
    for (i = 0; i < arr.Length; i++) {
        sum += arr[i];
    }
    // }
    

    对于那些老板不喜欢在凝视中看见大括号的情况,用begin、end来取代:

    // Begin of "Do the second thing"
    for (i = 0; i < arr.Length; i++) {
        sum += arr[i];
    }
    // End of "Do the second thing"

    准确使用凝视中的大括号或者begin、end的优点是。当一大块代码中还有嵌套凝视时,依旧能够清晰地表示出一段范围。

    另外,我个人的准则是不给凝视加上step 1、step 1.1、step 2之类字样,原因非常easy,一旦在原有的步骤里面插入一个新步骤。那么从这个步骤往后全部步骤的编号都要调整,太费事儿。

    另外,对于复杂的算法或业务需求。也须要加凝视说明,以免在数年后回头来看这段代码时。忘记当初为什么是这样写的了。对于算法,要说清楚这个算法的需求是什么,它是怎么设计的,什么样的输入须要被处理,处理的原理是什么。等等。对于业务逻辑,须要说明要支持的输入情况(包含全局、静态变量和数据库等环境数据的情况)、全部的处理步骤在业务流程中的含义、处理完毕之后对数据和业务状态带来什么影响,等等。以算法为例,以下的样例来自一个拓扑排序之前检查图是否有循环的方法:

    def detect_loop(graph, o_loop):
        """
        detect_loop:
            Detects any loop in a graph.
    
        Parameters:
            graph - the graph to test
            o_loop - a list to receive the looping nodes
    
        Return value:
            True if a loop has been detected. False otherwise.
        """
        # We are using depth-first search to find loops.
        #
        # If we did not implement recursive calls, we could use trace-back in a
        # non-recursive manner; python default recursion limit is about 900, which
        # is in general enough here, as the dependency we analyze is usually less
        # than 100.
        #
        # Due to the fact that if we traverse a non-tree directed acyclic graph
        # (DAG), we may end up in a time complexity of O(2 ^ n), we make a deep
        # copy of the graph first, and make it into a tree. During the process, we
        # can detect loops. The time complexity is O(n ^ 2) where n is the number
        # of nodes.
        result = False
        copied_graph = GraphNode.deep_copy_graph(graph)
        # Cases:
        # Root 1 leads to a loop--will be detected and the function will return.
        #     The loop will have a link pointing back to an ancestor node or the
        #     current node itself.
        # Root 1 leads to a DAG--any link to a visited node (cannot be an ancestor
        #     node or the current node itself) will be detected and removed, and
        #     made into a tree
        # Root 1 leads to a DAG (call it DAG1), root 2 links to DAG1--no loop can
        #     involve DAG1. Reason: suppose there is a loop involving DAG1, then
        #     from a node that is a part of the intersection, we can go back to
        #     it through the links, thus making DAG1 not a DAG--contradiction.
        # So if root 2 leads to a loop--the loop will be detected by checking the
        #     DFS traversal stack
        # If all of root 1..n-1 lead to DAGs, and root n leads to a loop, it will
        #     be detected only there
        #
        # accessed: used to mark accessed nodes in the DAG. Its members are the
        # names of accessed nodes. When an accessed node is met through a link,
        # the traversal returns and the link is removed, because the link should
        # not be added to the tree.
        accessed = set() # of node name (string)
        # traversal_stack: is used to record the loop to show to the user
        traversal_stack = []
        for node_name in copied_graph:
            result = detect_loop_rec(copied_graph, node_name, accessed, traversal_stack)
            if result:
                # Loop detected
                o_loop.extend(traversal_stack)
                break
        return result
    

    第六点,要为简化、抽象和缩写的变量名或函数名,凝视其全称及其含义。

    比方,你用winnt4wks来代表Microsoft Windows NT Workstation 4 i386 Multiprocessor Free的时候。你就应该吧这个全名用凝视的方式写在右边(或上方):

    // winnt4wks: Microsoft Windows NT Workstation 4 i386 Multiprocessor Free
    object winnt4wks;

    注意上面的样例中,假设凝视是写在变量名上方的,那么首先要用这个变量名先导,然后加个冒号,接下来才是解释。

    第七点,不要为不言自明的代码加凝视。

    这一点非常自然。比方一段代码大家都知道是干啥的。就根本不是必需写凝视。写了凝视反倒是干扰视听。弄不好将来改动代码时还须要维护。或者忘了维护。造成后来者被误导的情况。

    比方,以下代码的凝视在生产代码中就全然是不是必需的:

    // Loop and print every element of the array
    for (i = 0; i < arr.Length; i++) {
        Console.WriteLine(arr[i]);
    }

    第八点,不要为频繁变化的代码写冗余的凝视。前面说到过,凝视和代码所表达的含义可能有一点重合。

    此时,假设某段代码常常改,那么。基本上能够肯定的是,代码的负责人知道这段代码是什么含义,由于近期刚刚改过。

    那么。必要的凝视仍然要加,可是与代码含义反复的、冗余的信息就不那么必要了,能够省略。

    作为一名程序猿,掌握了以上要点,就能写出好的凝视,让自己的代码变得更加易读、易维护。

  • 相关阅读:
    BeautifulSoup的高级应用 之.parent .parents .next_sibling.previous_sibling.next_siblings.previous_siblings
    zoj 1655 单源最短路 改为比例+最长路
    localstorage
    unix中文件I/O
    TextView超链接
    Linux环境安装phpredis扩展
    可替代google的各种搜索引擎
    otto源代码分析
    8天学通MongoDB——第二天 细说增删查改
    MongoDB (十一) MongoDB 排序文档
  • 原文地址:https://www.cnblogs.com/clnchanpin/p/7388407.html
Copyright © 2020-2023  润新知