• 编写自文档化的代码


    原文链接:http://www.cnblogs.com/anderslly/archive/2009/06/21/write-self-documenting-code.html

    编写自文档化的代码

    文所以载道也。  —— 宋·周敦颐《通书·文辞》

    对于我们程序员来说,我们的工作也是写作——几乎每天都要写代码;而且还要载“道”,不仅仅要满足客户的需求,还要让代码具有高度的可读性,这样其他的程序员可以更容易地对代码进行修改和扩展。

    按这样的要求,我们需要为代码编写足够的文档,也就是将代码“文档化”。常见的做法有两种,外部文档和注释。

    外部文档

    外部文档指的是在代码文件之外编写的附加文档,比如在Word文档中采用大量的篇幅(如UML图、表格)来设计或记录相关的包、类型、类型成员、成员参数之类的信息。这看起来很规范,但如果你用过这种方式,一定会讨厌它。这种方式的主要问题在于:

    1)增加很多额外的工作:编写代码本身的压力已经很大,在压力之下,我们往往选择做那些最必需的事情,就是实现功能,如果时间紧急,编写文档就可能是草草了事了。

    2)文档需要与代码保持同步:即使你在开始认真编写了文档,后来代码有了修改和扩展(这是不可避免的,即使采用所谓的冻结需求),那么文档就需要更新,否则就会提供误导信息。

    3)大量的文档难以管理:如果代码量较大,那么本身就需要大量的文档;同时文档也需要进行版本管理,那么就产生了不同版本的文档;另外这些文档基本上是一些简单的文本。如此一来,要在这些文档中找到所需的信息,难上加难。

    文档具有这些问题,一个重要的原因是,它们离代码太“”了。我们可以将它们搬到代码文件里面,这就是第二种做法:注释。

    注释

    程序员对文档往往比较抵触,对注释的态度就温和多了,甚至相当一部分人支持编写大量的注释。的确,如果在IDE中看到分布合理的绿色代码块(注释文本的常见颜色),人们会感觉比较舒服,如果满屏幕全是代码,心里不免会犯怵。

    从语法的角度来看,注释就是编译器将忽略不计的源代码块。所以,在这里你想写什么就写什么。

    从语义的角度来看,注释是昏暗泥泞的小路和明亮通畅的大道之间的区别。注释是对其所处位置的代码的解释,它可以强调某些特定问题、描述某个复杂算法、对代码进行合理分隔、协助进行维护的程序员(这个人有可能是你自己)。由此可把注释看作是代码的一种“内部文档”。

    那是不是就需要大量的注释呢?至少我们曾经被这样教导过,但事实并非如此。不知道你的习惯怎样,我在阅读代码的时候,看到注释一般会先看注释,我在假定这些注释对代码提供了附加的价值。但我发现,注释往往很随意,甚至有可能误导别人。你可以说,这是注释编写者的问题,注释是无辜的,但必须承认的是,注释比代码更容易说谎。究其原因,注释虽然离代码很近,但仍然是一种文档,它具有与外部文档类似的问题:

    1)增加很多额外的工作

    2)需要与代码保持同步

    3)大量的注释可能会妨碍代码的阅读:注释在那里,人们不能置之不理,如果注释太多就成阻碍了。《重构》一书认为注释过多是一种“坏味道”。

    看来,注释也有不少问题,它们离代码仍有距离。能否将“文档”与代码的距离再拉近一点?那该怎么做?

    先来考虑我们为什么要添加注释。这往往是因为代码本身不容易让人看懂,也就是说代码的意图和表现有距离,所以才需要使用注释。如果能够做到让代码本身就体现出意图,是不是就不需要注释了?这种方式就是本文的主题:代码的自文档化。(需要注意的是,自文档化能够取代大多数的注释,但并不能100%取代)

    什么是代码的自文档化(Self Documenting Code)?

    唯一能完整并正确地描述代码的文档时代码本身,通常情况下,这也是你能获得的唯一文档。因此,我们应当努力使代码成为良好的文档,一种人人可以读懂的文档。这也就是通常所说的良好的可读性,做到了这一点,犯错的可能性就降低了,同时代码的维护成本也降低了——人们不需要花太多时间去熟悉你的代码。(更多信息,可以参考Ward Cunningham的wiki

    代码自文档化的技巧

    我们可以采用多种方式来提高代码的可读性。其中一些技巧是非常基础的,我们在编程之初已学习过,而有些则更为巧妙。这里首先给出两个例子,加深一下你对代码可读性好坏的印象。

    让人郁闷的代码
    static int fval(int i)
    {
        
    int ret = 2
    ;
        
    for (int n1 = 1, n2 = 1, i2 = i - 3; i2 >= 0--
    i2)
        {
            n1 
    = n2; n2 = ret; ret = n1 +
     n2;
        }
        
    return (i < 2? 1
     : ret;
    }

     


    static int fibonacci(int position)
    {
        
    if (position < 2
    )
        {
            
    return 1
    ;
        }

        
    int previousButOne = 1
    ;
        
    int previous = 1
    ;
        
    int answer = 2
    ;

        
    for (int n = 2; n < position; n++
    )
        {
            previousButOne 
    =
     previous;
            previous 
    =
     answer;
            answer 
    = previousButOne +
     previous;
        }

        
    return
     answer;
    }


    看到第一个函数,是不是想骂人了?骂完了,你还得跟踪代码才能了解代码在干什么;而第二个则很清晰,你会由衷的感谢代码的作者。事实上,两者的功能一样,都是计算斐波纳契序列的值。所以说,为了不被人骂,我们也得注重代码的可读性:) 下面来看看有哪些具体的技巧。

    1)好的代码样式

    代码的样式极大地影响着代码的清晰度。

    a)让“正常”流程贯穿你的代码,异常情况不该扰乱它。比如在使用if…else…时。

    b)避免过多的嵌套

    c)还有其它更为基本的,比如代码行不要过长,一行内只有一个语句等等(见前面的坏例子)。

    2)选择有意义的名称

    这一点怎么强调都不过分。如果文件、类型、函数、变量、参数都有了适当的名称,那么将减少大量的注释。这种方式可以让我们编写更接近于自然语言的代码。

    关于有意义的名称,可以写另一篇文章了。这里仅作简单介绍。

    a)属性、变量和参数命名

    如果是名词性的内容,用名词命名。如user,userName(考虑一下两者的不同),还有elapsedTime等等。

    如果是逻辑性的内容,要体现出来。比如isEmpty,isRunning等等。对比下面的代码:

    使用良好的命名
    if(isEmpty)
    {
    }

    if
    (flag)
    {
    }

    b)函数/方法命名

    函数往往表示行为,所以在逻辑上应包含一个动词,如Reset、Start等等。

    有时我们还可以隐含参数和返回值的信息,如看到ContainsKey,我们可以了解它的参数应当是key,而返回值为bool类型的值,表示是否包含该key。

    这样的例子,从.NET Framework的类库中可以大量看到,要学习这些规范,建议阅读《.NET设计规范》。

    回头看看那个可恶的fval(),你能看出它是干什么的吗?

    c)类型和命名空间的命名

    这些也有一些约定俗成的东西,比如Attribute、Exception和接口命名等等,最重要的是团队内保持一致。在此不再赘述。

    d)文件的命名

    一般而言,文件的名字应该能反映出所包含的类型。但在.NET中,有partial类的概念,同一个类的代码可能分布在不同文件中,这时建议先对代码进行合理的分组,其中一个主文件,它的名称与类型名称相同,其它文件以此为基础,并且整个团队保持一致。比如ASP.NET中的Default.aspx.cs和Default.aspx.designer.cs。

    关于命名,还有一个要注意的地方:并非写得越多越好。比如PersonClass,DataObject,还有常见的匈牙利命名法,iCount,strName这样的都该放弃,别人从count和name就可以了解到足够的信息了。

    3)分解为原子函数

    重构》一书曾提到,“当我看到一段需要注释才能让人理解意图的代码,我就会将它放进一个独立的函数中”。这种方式衍生的一个问题是,函数细化到什么程度?

    a)一个函数,一种操作:同时为此操作起一个容易理解的名字,这时,注释还需要吗?

    b)减少副作用:副作用总需要额外的说明

    c)简短:虽然你是想只包含一种操作,但发现最后的代码竟然有数百行,那说明还不够细化,继续分解,如此递归下去

    4)通过语言层面的机制减少误解

    a)如果参数是非负整数,那么考虑用uint来代替int,否则就需要添加注释来说明

    b)如果需要一个不能被改变的值,就使用readonly或const

    c)使用枚举描述一组相关的值

    d)选择合适的修饰符,当你为方法使用的修饰符是public或private时,别人读到时的感受肯定不同

    5)使用常量

    在阅读代码的时候,映入眼帘的如果是这样的代码:if(counter == 76),相信你会变得紧张、不知所措,这样的数字称为魔数(Magic Number)。我们的程序不需要像魔术那样让人看不懂,我们现在讨论的是提高可读性。为它添加一个有意义的名字吧,比如bananasPerCake,它的一个额外好处是带来了更好的可维护性,因为现在只要修改一处。

    这里针对不仅仅是数字,而是任何让人看不懂的硬编码。

    6)强调重要的代码

    有时代码的读者不需要了解所有信息,这时可以:

    a)在类中按一定的顺序进行声明。比如public的成员放在前面,这往往是读者最需要了解的,可以考虑的一种方式是使用C#中的#region。

    b)隐藏不重要的信息,这时自然就强调了重要的信息了

    c)限制嵌套的条件语句的数量,否则就容易掩盖那些重要的条件分支了

    7)考虑上下文信息

    在一个地方就只考虑它该考虑的事情。比如异常处理,在团队内可以约定,统一在业务逻辑层进行处理,而不是在任何地方都进行处理。

    关于相关的技巧就写到这里,相信还会第8, 9, …, N条可以补充,不过在这些之后如果还是不满足怎么办?这时候考虑使用注释。

    N+1)编写有意义的注释

    如果不能以代码本身的方式提高可读性,最后才考虑注释。关于编写注释,又可以写一篇文章了,这里只想说,把注释作为最后一种选择,而且添加的注释应该给代码带来附加价值

    不过,我们总有犯错误的时候,怎样补救呢?重构代码吧,为了自己,也为了其他要阅读你代码的人。不过,可以使用一些好用的工具,免得在重构的时候继续犯错,这里推荐一个免费的工具:CodeRush Xpress,我曾做过一个介绍。它极大地增强了VS的重构能力,对于上面提供的几点比如代码样式、重命名、提取方法、提取常量都提供了良好的支持。

    最后,介绍一下我最近在考虑的一种编码方式,希望它能帮助你编写更具可读性的代码。

    前面说了这么多,其目的就是让代码具有更好的可读性,那最有可读性的文本是什么呢?当然是自然语言,但我们不能以自然语言编码(至少现在还不行)。那么退一步,考虑伪代码,它介于自然语言和编程语言之间,形式非常灵活。如果能以伪代码的方式编码,是不是也很好?这里还是借助于CodeRush Xpress。

    以接近伪代码的方式编程

    以我最近遇到的一个问题为例,不过要简化一下。客户需要以编程的方式从VSS上Checkout一个文件,现在了解到实现此功能需要的信息有文件的服务器端路径,本地路径,本次操作的注释信息,还有如果本地文件如果是可写的,要提示用户是否覆盖,暂时先考虑这些。首先对于输入的服务器端路径可能对应文件夹,也可能对应文件,要分开考虑,此时可以写出如下的代码:

    C# Code
    public void Checkout()
    {
        
    string
     serverPath;
        
    string
     localPath;
        
    string
     comment;

        
    if
     (IsFile(serverPath))
        {
            CheckoutFile(serverPath, localPath, comment);
        }
        
    else

        {
            CheckoutFolder(serverPath, localPath, comment);
        }
    }


    这样需要三个新方法:IsFile、CheckoutFile和CheckoutFolder,注意它们的命名表达了我的思路(意图)。在VS中可以生成它们的方法存根:

    generate-method-stub

    IsFile比较简单,先不管它,现在考虑CheckoutFile的实现。在Checkout一个文件时,要考虑本地文件可写的情况,可以写出如下的代码:

    C# Code
    private void CheckoutFile(string serverPath, string localPath, string comment)
    {
        
    if
     (IsWritable(localPath))
        {
            
    if (!
    WantToGetLocalCopy())
            {
                CheckoutWithoutGettingLocalCopy(serverPath, localPath, comment);
            }
        }

        CheckoutAndGetLocalCopy(
    serverPath, localPath, comment);
    }


    这里又添加了几个方法,方法名同样表达了思路。到这一步方法们都比较小了,可以直接实现,对于CheckoutFolder,也按同样方式实现。最后检查一下,开始声明的几个局部变量应该作为参数,把它们“提升”为参数:

    promote-to-parameter

    这样完成了Checkout方法。这种方式的好处在于基本上按照自然的思维方式来编码,把想做的事情命名为方法即可,可读性自然较高。而且,最终将方法分解、细化,符合前面的第三条。建议尝试一下这种方法。

    小结

    文档的可读性是如此重要,以至于可以作为程序员是否职业的一个标准。通过本文的分析可以了解到,使代码自文档化是提高可读性的最佳选择。

    参考

    编程匠艺

    重构

    Self Documenting Code by c2.com

    http://en.wikipedia.org/wiki/Self-documenting

    作者:Anders Cui
    出处:http://anderslly.cnblogs.com/
    本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接,否则保留追究法律责任的权利。
  • 相关阅读:
    使用javap分析Java的字符串操作
    使用javap深入理解Java整型常量和整型变量的区别
    分享一个WebGL开发的网站-用JavaScript + WebGL开发3D模型
    Java动态代理之InvocationHandler最简单的入门教程
    Java实现 LeetCode 542 01 矩阵(暴力大法,正反便利)
    Java实现 LeetCode 542 01 矩阵(暴力大法,正反便利)
    Java实现 LeetCode 542 01 矩阵(暴力大法,正反便利)
    Java实现 LeetCode 541 反转字符串 II(暴力大法)
    Java实现 LeetCode 541 反转字符串 II(暴力大法)
    Java实现 LeetCode 541 反转字符串 II(暴力大法)
  • 原文地址:https://www.cnblogs.com/yangjunwl/p/1508168.html
Copyright © 2020-2023  润新知