读代码整洁之道总结

术语总结

TDD(Test Driven Development):测试驱动开发

理念总结

整洁的代码力求集中。每个函数、每个类和每个模块都全神贯注千一事,完全不 受四周细节的干扰和污染。

•    能通过所有测试; 
•    没有重复代码;
•    体现系统中的全部设计理念;
•    包括尽量少的实体,比如类、方法、函数等;

命名规则和例子
不要用专有名词
  • hp、aix和sco都不该用做变量名,因为它们都是UNIX平台或类UNIX平台的专有名称。
不要用误导性名称
  • 别用accountList来指称一组账号,除非它真的是List类型。List 一词对程序员有特殊意义。如果包纳账号的容器并非真是个List,就会引起错误的判断。所以,用accountGroup或 bunchOfAccounts,甚至直接用accounts都会好一些。
  • 如果用小写字母I和大写字母O作为变量名,尤其是在组合使用时,它们看起来完全像是数字1和0,
避免用废话或者数字区分
  • 数字系列命名(al、a2, aN),这样的名称纯属误导:完全没有提供正确信息,没有提供导向作者意图的线索。
  • 废话是另一种没意义的区分。假设你有一 个Product类。如果还有一个Productlnfo 或ProductData类,名称虽然不同,意思却无区别。Info和Data就像a、an和the一样,是意义含混的废话。
  • 如果缺少明确约定,变量moneyAmount就与money没区别,customerlnfo与customer 没区别,accountData与account没区别,theMessage也与message没区别。要区分名称,就要以读者能鉴别不同之处的方式来区分。
使用可以读得出来的名称

不要搞自造词或者缩写词,用的名称需要具有普遍性。

举个反例:程序里有genymdhms(生成日期,年、月、日、时、分、秒)的方法,有经验的开发会一眼看出这个方法是干啥的,但这读不出来,你试试读读看?听起来搞不搞笑?

以下是两个正反例子,可以自行体会。

class DtaRcrdl02 {
    private Date genymdhms; 
    private Date modymdhms; 
    private final String pszqint = "102";
} 
class Customer { 
    private Date generationTimestamp; 
    private Date modificationTimestamp;
    private final String recordid = "102"; 
}
使用可搜索的名称

长名称胜于短名称,搜得到的名称胜于用自造编代码写出的名称。名称长短应与其作用域大小相对应。

以下例子中,WORK_DAYS_PER_WEEK要比数字5好找得多,对吧?

for (int j=O; j<34; j++) { 
    s += (t[j]*4)/5; 
}

int realDaysPeridealDay = 4; 
const int WORK_DAYS_PER_WEEK = 5; 
int sum = 0; 
for (int j=O; j < NUMBER_OF_TASKS; j++) { 
    int realTaskDays = taskEstimate[j] * realDaysPeridealDay; 
    int realTaskWeeks = (realdays / WORK_DAYS_pER_WEEK); 
    sum += realTaskWeeks; 
}
避免使用编码

编码已经太多, 无谓再自找麻烦。把类型或作用域编进名称里面, 徒然增加了解码的 负担。没理由要求每位新人都在弄清要应付的代码之外(那算是正常的),还要再搞懂另一种编码 “语言”。 这对于解决问题而言,纯属多余的负担。带编码的名称通常也不便发音, 容易打错。

避免思维映射

不应当让读者在脑中把你的名称翻译为他们熟知的名称。

单字母变量名就是个问题。 在作用域较小、 也没有名称冲突时,循环计数器自然有可能
被命名为i或j或k。(但千万别用字母l!)这是因为传统上惯用单字母名称做循环计数器。

然而,在多数其他情况下,单字母名称不是个好选择;读者必须在脑中将它映射为真实概念。仅仅是因为有了a和b, 就要取名为c, 实在并非像样的理由。

专业程序员 善用其能,编写其他人能理解的代码。

类名

类名和对象名应该是名词或名词短语,如Customer、WikiPage、Account和AddressParser。 避免使用Manager、Processor、Data或Info这样的类名。类名不应当是动词。

方法名

方法名应当是动词或动词短语,如postPayment、deletePage或save。属性访问器、修改器和断言应该根据其值命名,并依Javabean标准加上get、set和is前缀。

string name = employee.getName(); 
customer.setName("mandy"); 
if (paycheck. is Posted())... 

重载构造器时,使用描述了参数的静态工厂方法名。例如,

Complex fulcrumPoint = new Complex (23. 0); 
// => 更好的写法
Complex fulcrumPoint = Complex.FromRealNumber(23.0); 

别扮可爱

扮可爱的做法在代码中经常体现为使用俗话或俚语。

如果有个函数叫HolyHandGrenade。翻译过来就是圣手手雷,虽然搞笑但不直译,改成Deleteltems跟让人一目了然。

再例如,别用whack()(whack是劈砍的意思)来表示kill()。别用eatMyShorts()(eatMyShorts是去死吧的意思)这类与文化紧密相关的笑话来表示abort()。

每个概念对应一个词

函数名称应当独一无二,而且要保持一致。反之,在同一堆代码中有controller,又有manager,还有driver,就会令人困惑。DeviceManager 和Protocol-Controller之间有何根本区别?为什么不全用controllers或managers?他们都是 Drivers吗?这种名称,让人觉得这两个对象是不同类型的,也分属不同的类。

那些会用到你代码的程序员, 一以贯之的命名法简直就是天降福音。

别用双关语

避免将同一单词用于不同目的。同一术语用于不同概念, 基本上就是双关语了。 如果遵循“一词一意”的规则,可能在好多个类里面都会有add方法。 只要这些add方法的参数列表和返回值在语义上等价, 就一切顺利。

但是, 可能会有人决定为 “保持一致” 而使用add这个词来命名, 即便并非真的想表示这种意思。比如, 在多个类中都有add方法,该方法通过增加或连接两个现存值来获得新值。假设要写个新类,该类中有一个方法,把单个参数放到群集(collection)中。该把这个方法叫做add吗?这样做貌似和其他add方法保持了一致, 但实际上语义却不同, 应该用insert 或append之类词来命名才对。把该方法命名为add,就是双关语了。

使用解决方案领域名称

只有程序员才会读你的代码。所以,尽管用那些计算机科学(Computer Science, CS) 术语、算法名、模式名、数学术语,换句话说就是程序员熟悉的术语。

使用源自所涉问题领域的名称

如果不能用程序员熟悉的术语来命名,就采用从所涉问题领域而来的名称吧。 至少负责维护代码的程序员就能去请教领域专家了。如果所涉问题领域更为贴近代码, 应当采用源自问题领域的名称。

添加有意义的语境

你需要用有良好命名的类 、函数或名称空间来放置名称, 给读者提供语境。 如果没这么做,给名称添加前缀就是最后一招了。

不要添加没用的语境

比如,你在GSD应用程序中的记账模块创建了一个表示邮件地址的类,然后给该类命名为GSDAccountAddress。你的客户联络应用中需要用到邮件地址,你会用 GSDAccountAddress吗?这名字听起来没问题吗?在这17个字母里面,有10个字母纯属多余和与当前语境毫无关联。

只要短名称足够清楚,就要比长名称好。别给名称添加不必要的语境。

对于Address类的实体来说,accountAddress和customerAddress都是不错的名称,不过用在类名上就不太好了。Address是个好类名。如果需要与MAC地址、端口地址和Web地址相区别,我会考虑使用PostalAddress、MAC和URL。这样的名称更为精确,而精确正是命名的要点。 

函数
函数的第一规则是要短小

if语句、else语句、while语句等,其中的代码块应该只有一行。该行大抵应该是一个函数调用语句。这样不但能保持函数短小,而且,因为块内调用的函数拥有较具说明性的名称。函数的缩进层级不该多于一层或两层。当然,这样的函数易于阅读和理解。

函数应该做一件事。做好这件事。只做这一件事。

 要判断函数是否不止做了一件事,还有一个方法,就是看是否能再拆出一个函数,该函数不仅只是单纯地重新诠释其实现。做一件事的函数无法被合理地切分为多个区段。

要确保函数只做一件事,函数中的语句都要在同一抽象层级上。如果函数中混杂不同抽象层级,往往会让人迷惑。

使用描述性的名称

函数越短小、功能越集中,就越便于取个好名字。

但也别害怕长名称。长而具有描述性的名称,要比短而令人费解的名称好。长而具有描述性的名称,要比描述性的长注释好。

别害怕花时间取名字。当尝试不同的名称,测试其阅读效果。如果觉得有更好的,直接编辑器及一件替换,也不是一件太麻烦的事。

命名方式要保持一致。使用与模块名一脉相承的短语、名词和动词给函数命名。

函数参数

最理想的参数数量是0(零参数函数),其次是一(单参数函数),再次是二(双参数函数),应尽量避免三(三参数函数)。有足够特殊的理由才能用三个以上参数(多参 数函数) ——所以无论如何也不要这么做。

从测试角度上来讲,如果参数太多,那么测试覆盖范围值的组合就让人生畏了。

向函数传入布尔值的做法真的不建议做。这样做感觉像大声宣布本函数不止做一件事(要做true和false两件事。正确的做法是要分开两个函数分别处理true和false的事件。

给函数取个好名字,能较好地解释函数的意图,以及参数的顺序和意图。

一元函数怎么取名:函数和参数应当形成一种非常良好的动词/名词对形式。

例如,write(name)就相当令人认同。 不管这个 “name"是什么,都要被 “ write”。 更好的名称大概是writeField(name),它告诉我们,“name"是一个 “ field ”。

无副作用
public boolean checkPassword (String userName, String password) { 
    User user = UserGateway.findByName(userName); 
    if (user ! = User.NULL) { 
        String codedPhrase = user.getPhraseEncodedByPassword(); 
        String phrase = cryptographer.decrypt(codedPhrase, password); 
        if ("Valid Password".equals(phrase)) { 
            Session.initialize(); //偷偷做初始化
            return true; 
        }
    return false; 
}

看以上反例,函数名叫checkPassword,也没说要初始化会话,当还是偷偷做了,万一其他不熟悉这部分代码的同事直接调用了该函数,就会有抹除会话数据的风险。虽然可以重命名为checkPasswordAndlnitializeSession,当也违反了“只做一件事的规则”。

输出参数

一个函数的参数最好是真的是这个函数的输入而非输出,因为大家自然而然都会把这个参数作为输入去阅读代码。

分隔指令与询问

函数要么做什么事, 要么回答什么事,但二者不可得兼。函数应该修改某对象的状态,或是返回该对象的有关信息。两样都干常会导致混乱。 

对于以下语句,你第一时间是怎么理解的?是在问usemame属性值是否之前已经设置为unclebob吗?或者它是在问usemame属性值是否成功设置为unclebob呢?从这行调用很难判断其含义, 因为set是动词还是形容词并不清楚。

if (set("username", "unclebob"))...

真正的解决方案是把指令与询问分隔开来,防止混淆的发生:

if (attributeExists ("username")) { 
    setAttribute("username", "unclebob"); 
}
使用异常替代返回错误码

从指令式函数返回错误码轻微违反了指令与询问分隔的规则,导致更深层次的嵌套结构。当返回错误码时,就是在要求调用者立刻处理错误。

if (deletePage(page) == E_OK) { 
    if (registry.deleteReference(page.name) == E_OK) { 
        if (configKeys.deleteKey(page.name.makeKey()) == E_OK) { 
            logger.log("page deleted"); 
        } else { 
            logger.log("configKey not deleted"); 
        }
    } else { 
        logger.log("deleteReference from registry failed"); 
    }
} else { 
    logger.log("delete failed"); 
    return E_ERROR; 
}

如果使用异常替代返回错误码,错误处理代码就能从主路径代码中分离出来得到简化:

try{
    deletePage(page);
    registry.deleteReference(page.name); 
    configKeys.deleteKey(page.name.makeKey()); 
}catch (Exception e) { 
    logger.log(e.getMessage ()); 
}
抽离Try/Catch代码块

Try/catch代码块丑陋不堪。它们搞乱了代码结构, 把错误处理与正常流程混为一趟,最好把try和catch代码块的主体部分抽离出来, 另外形成函数。

public void delete(Page page) { 
    try { 
        deletePageAndAllReferences(page); 
    }catch (Exception e) { 
        logError(e}; 
    }
}

private void deletePageAndAllReferences(Page page) throws Exception ( 
    deletePage(page); 
    registry.deleteReference(page.name); 
    configKeys.deleteKey(page.name.makeKey()); 
}

private void logError(Exception e) { 
    logger.log(e.getMessage()); 
}

在上面的例子中,delete函数只与错误处理有关。很容易理解然后就忽略掉。deletePageAndAIIReference函数只与完全删除一个page有关。错误处理可以忽略掉。

这样有了这样美妙的区隔,代码就更易于理解和区分。

错误处理就是一件事

处理错误的函数不该做其他事。 这意味着(如上例所示)如果关键字try在某个函数中存在,它就该是这个函数的第一个单词,而且在catch/family代码块后面也不该有其他内容。

Error.java 依赖磁铁

返回错误码通常暗示某处有个类或是枚举,定义了所有错误码。

public enum Error { 
    OK,
    NVALID, 
    NO_SUCH, 
    LOCKED, 
    OUT_ OF _RESOURCES, 
    WAITING_FOR_EVENT
}

这样的类就是一块依赖磁铁(dependency magnet);其他许多类都得导入和使用它。 当Error枚举修改时,所有这些其他的类都需要重新编译和部署。这对Error类造成了负面压力。程序员不愿增加新的错误代码,因为这样他们就得重新构建和部署所有东西。 于是他们就复用旧的错误码, 而不添加新的。

使用异常替代错误码,新异常就可以从异常类派生出来,无需重新编译或重新部署

别重复自己 

许多原则与实践规则都是为控制与消除重复而创建。例如, 面向对象编程是如何将代码集中到基类,从而避免了冗余。面向方面编程(Aspect Oriented Programming)、面向组件编程(Component Oriented Programming) 多少也都是消除重复的一种策略。看来, 软件开发领域的所有创新都是在不断尝试从源代码中消灭重复。

结构化编程

每个函数、函数中的每个代码块都应该有一个入口、一个出口。

遵循这些规则,意味着在每个函数中只该有一个 return语句,循环中不能有 break或 continue语句, 而且永永远远不能有任何 goto语句。

我们赞成结构化编程的目标和规范,但对于小函数, 这些规则助益不大。只有在大函数中, 这些规则才会有明显的好处。

所以, 只要函数保持短小, 偶尔出现的return、break 或continue 语句没有坏处,甚至还比单入单出原则更具有表达力。另外一方面, goto只在大函数中才有道理, 所以应该尽量避免使用。

如何写出这样的函数

写代码和写别的东西很像。在写论文或文章时,你先想什么就写什么,然后再打磨它。初稿也许粗陋无序, 你就斟酌推敲, 直至达到你心目中的样子。 我写函数时, 一开始都冗长而复杂。有太多缩进和嵌套循环。有过长的参数列表。名称是随意取的, 也会有重复的代码。不过我会配上一套单元测试, 覆盖每行丑陋的代码。 然后我打磨这些代码,分解函数、修改名称、消除重复。我缩短和重新安置方法。有时我还拆散类。同时保持测试通过。最后, 遵循本章列出的规则, 我组装好这些函数。我并不从一开始就按照规则写函数。我想没人做得到。

注释

存在注释并不值得庆祝。 如果你发现自己需要写注释,再想想看是否有办法翻盘,用代码来表达。

注释存在的时间越久,就离其所描述的代码越远, 越来越变得全然错误。原因很简单。程序员不能坚持维护注释。

注释不能美化糟糕的代码

写注释的常见动机之一是糟糕的代码的存在。但我要说的是,与其花时间编写解释你搞出的糟糕的代码的注释,不如花时间清洁那堆糟糕的代码。

用代码来阐述

有时, 代码本身不足以解释其行为。不幸的是, 许多程序员据此以为代码很少——如果有的话——能做好解释工作。这种观点纯属错误。

以下两段代码,更喜欢哪个呢?很多时候, 简单到只需要创建一个描述与注释所言同一事物的函数即可。

// Check to see if the employee is eligible for full benefits
if ((employee. flags & HOURLY_FLAG) &&.(employee. age > 65))
if (employee. isEligibleForFullBenefits())
好注释

有些注释是必须的, 也是有利的。不过要记住, 唯一真正好的注释是你想办法不去写的注释。

哪些是好注释呢?

  • 法律信息

        公司代码规范要求编写与法律有关的注释。例如,版权及著作权声明就是必须和有理由在每个源文件开头注释处放置的内容。

        下例是我们在 FitNesse 项目每个源文件开头放置的标准注释。

// Copyright (C) 2003,2004,2005 by Object Mentor, Inc. All rights reserved.

// Released under the terms of the GNU General Public License version 2 or later.

        这类注释不应是合同或法典。如果可以,就指向一份标准许可或其他外部文档, 而不要把所有条款放到注释中。

  • 提供信息的注释

        用注释来提供基本信息也有其用处。例如,以下注释解释了某个抽象方法的返回值:

// Returns an instance of the Responder being tested.
protected abstract Responder responderInstance();

这类注释有时管用, 但更好的方式是尽量利用函数名称传达信息。比如,在本例中,只要把函数重新命名为 responderBeingTested, 注释就是多余的了。

  • 对意图的解释

有时, 注释不仅提供了有关实现的有用信息,而且还提供了某个决定后面的意图。

在下例中, 我们看到注释反映出来的一个有趣决定。在对比两个对象时,作者决定将他的类放置在比其他东西更高的位置。

public int compareTo(Object o){
    if(o instanceof WikiPagePath){
        WikiPagePath p = (WikiPagePath) o;
        String compressedName = StringUtil. join(names, "");
        String compressedArgumentName = StringUtil. join(p. names, "");
        return compressedName.compareTo(compressedArgumentName);
    }
    return 1; // we are greater because we are the right type.
}
  • 阐释

注释把某些晦涩难明的参数或返回值的意义翻译为某种可读形式,也会是有用的。通常,更好的方法是尽量让参数或返回值自身就足够清楚;但如果参数或返回值是某个标准库的一部分,或是你不能修改的代码, 帮助阐释其含义的代码就会有用。

public void testCompareTo() throws Exception{
    WikiPagePath a = PathParser. parse("PageA");
    WikiPagePath ab = PathParser. parse("PageA. PageB");
    WikiPagePath b = PathParser. parse("PageB");
    WikiPagePath aa = PathParser. parse("PageA. PageA");
    WikiPagePath bb = PathParser. parse("PageB. PageB");
    WikiPagePath ba = PathParser. parse("PageB. PageA");
    assertTrue(a. compareTo(a) == 0); // a == a
    assertTrue(a. compareTo(b) != 0); // a != b
    assertTrue(ab. compareTo(ab) == 0); // ab == ab
    assertTrue(a. compareTo(b) == -1); // a <b
    assertTrue(aa. compareTo(ab) == -1); // aa< ab
    assertTrue(ba. compareTo(bb) == -1); // ba < bb
    assertTrue(b. compareTo(a) == 1); // b > a
    assertTrue(ab. compareTo(aa) == 1); // ab > aa
    assertTrue(bb. compareTo(ba) == 1); // bb > ba
}

当然,这也会冒阐释性注释本身就不正确的风险。

回头看看上例,你会发现想要确认注释的正确性有多难。这一方面说明了阐释有多必要,另外也说明了它有风险。所以, 在写这类注释之前, 考虑一下是否还有更好的办法,然后再加倍小心地确认注释正确性。

  • 警示

警告其他程序员会出现某种后果的注释也是有用的。例如,下面的注释解释了为什么要关闭某个特定的测试用例:

// Don't run unless you have some time to kill.
public void _testWithReallyBigFile(){
    writeLinesToFile(10000000);
    response. setBody(testFile);
    response. readyToSend(this);
    String responseString = output.toString();
    assertSubString("Content-Length: 1000000000",responseString);  
    assertTrue(bytesSent > 1000000000);
}
  • TOOO注释              

有时, 有理由用//TODO 形式在源代码中放置要做的工作列表。在下例中, TODO 注释解释了为什么该函数的实现部分无所作为, 将来应该是怎样。

// TODO-MdM these are not needed
// We expect this to go away when we do the checkout model
protected VersionInfo makeVersion() throws Exception{return null;} 

TODO 是一种程序员认为应该做,但由于某些原因目前还没做的工作。它可能是要提醒删除某个不必要的特性,或者要求他人注意某个问题。它可能是恳请别人取个好名字, 或者提示对依赖于某个计划事件的修改。

无论 TODO的目的如何,它都不是在系统中留下糟糕的代码的借口。 如今,大多数好IDE 都提供了特别的手段来定位所有 TODO 注释,这些注释看来丢不了。你不会愿意代码因为 TODO 的存在而变成一堆垃圾,所以要定期查看, 删除不再需要的。

  • 放大

注释可以用来放大某种看来不合理之物的重要性。

String listItemContent = match.group(3).trim();
// the trim is real important. It removes the starting
// spaces that could cause the item to be recognized as another list.
new ListItemWidget(this, listItemContent, this.level + 1);
return buildList(text.substring(match. end()));
  • 公共API中的Javadoc

没有什么比被良好描述的公共API更有用和令人满意的了。标准Java库中的Javadoc就是一例。没有它们, 写Java程序就会变得很难。

如果你在编写公共 API,就该为它编写良好的Javadoc。不过要记住本章中的其他建议。就像其他注释一样, Javadoc也可能误导、不适用或者提供错误信息。

 坏注释

大多数注释都属此类。通常,坏注释都是糟糕的代码的支撑或借口, 或者对错误决策的修正, 基本上等于程序员自说自话。

  • 喃喃自语

如果只是因为你觉得应该或者因为过程需要就添加注释,那就是无谓之举。如果你决定写注释, 就要花必要的时间确保写出最好的注释。

例中的注释大概确实有用。不过, 作者太着急,或者没太花心思。他的喃喃自语变成了一个谜团。

public void loadProperties(){
    try{
        String propertiesPath = propertiesLocation + "/" + PROPERTIES_FILE;
        FileInputStream propertiesStream = new FileInputStream(propertiesPath);
        loadedProperties. load(propertiesStream);
    }catch(IOException e){
        // No properties files means all defaults are loaded
    }
}

catch 代码块中的注释是什么意思呢?显然对于作者有其意义,不过并没有好到足够的程度。

很明显,如果出现IOException,就表示没有属性文件;在那种情况下,载入默认设置。但谁来装载默认设置呢? 会在对loadProperties. load之前装载吗? 抑或loadProperties. load捕获异常、装载默认设置、再向上传递异常? 再或loadProperties. load在尝试载入文件前就装载所有默认设置? 要么作者只是在安慰自己别在意 catch 代码块的留空? 或者——这种可能最可怕——作者是想告诉自己, 将来再回头写装载默认设置的代码?

我们唯有检视系统其他部分的代码, 弄清事情原委。任何迫使读者查看其他模块的注释,都没能与读者沟通好, 不值所费。

  • 多余的注释

简单函数其头部位置的注释全属多余。读这段注释花的时间没准比读代码花的时间还要长。

// Utility method that returns when this. closed is true. Throws an exception
// if the timeout is reached.
public synchronized void waitForClose(final long timeoutMillis)throws Exception{
    if(!closed){
        wait(timeoutMillis);
        if(!closed)
            throw new Exception("MockResponseSender could not be closed");
        }
    }
}

这段注释起了什么作用? 它并不能比代码本身提供更多的信息。它没有证明代码的意义,也没有给出代码的意图或逻辑。读它并不比读代码更容易。事实上,它不如代码精确,误导读者接受不精确的信息,而不是正确地理解代码。

  • 误导性注释

还是上一个例子,那多余而又有误导嫌疑的注释。 你有没有发现那样的注释是如何误导读者的? 在 this.closed 变为 true 的时候,方法并没有返回。方法只在判断到this. closed为 true的时候返回,否则, 就只是等待遥遥无期的超时,然后如果判断this. closed还是非true, 就抛出一个异常。 这一细微的误导信息,放在比代码本身更难阅读的注释里面, 有可能导致其他程序员快活地调用这个函数,并期望在 this. closed 变为 true时立即返回。那位可怜的程序员将会发现自己陷于调试困境之中, 拼命想找出代码执行得如此之慢的原因。

  • 循规式注释

所谓每个函数都要有 Javadoc 或每个变量都要有注释的规矩全然是愚蠢可笑的。这类注释徒然让代码变得散乱, 满口胡言,令人迷惑不解。

例如,要求每个函数都要有 Javadoc, 就会得到类似以下面目可憎的代码。这类废话只会搞乱代码, 有可能误导读者。

/*** @param title The title of the CD
* @param author The author of the CD    
* @param tracks The number of tracks on the CD
* @param durationInMinutes The duration of the CD in minutes
*/
public void addCD(String title, String author,  int tracks, int durationInMinutes) {
    CD cd = new CD();
    cd. title = title;    
    cd. author = author;
    cd. tracks = tracks;
    cd. duration = duration;
    cdList. add(cd);
}
  • 日志式注释

有人会在每次编辑代码时,在模块开始处添加一条注释。这类注释就像是一种记录每次修改的日志。

*Changes (from 11-Oct-2001)
*11-Oct-2001 : Re-organised the class and moved it to new packagecom. jrefinery. date (DG);
*05-Nov-2001 : Added a getDescription() method, and eliminated NotableDate class (DG);
*12-Nov-2001 : IBD requires. setDescription() method, now that NotableDate class is gone (DG); 

很久以前,还没有git管理的时候,这些记录还是挺需要的,但现在就不应该存在这种日志式的注释了。

  • 废话注释
/*** Returns the day of the month.
* @return the day of the month.*/
public int getDay0fMonth() {
    return day0fMonth;
}
  • 能用函数或变量时就别用注释

看看以下代码概要:

// does the module from the global list <mod> depend on the
// subsystem we are part of?
if (smodule.getDependSubsystems().contains(subSysMod.getSubSystem()))

可以改成以下没有注释的版本:

ArrayList moduleDependees = smodule.getDependSubsystems();
String ourSubSystem = subSysMod.getSubSystem();
if (moduleDependees. contains(ourSubSystem))
  • 位置标记

有时, 程序员喜欢在源代码中标记某个特别位置。

// Actions //

这种标记多数时候实属无理。鸡零狗碎, 理当删除——特别是尾部那一长串无用的斜杠。

这么说吧。如果标记栏不多,就会显而易见。所以,尽量少用标记栏,只在特别有价值的时候用。如果滥用标记栏,就会沉没在背景噪音中,被忽略掉。

  • 括号后面的注释

有时, 程序员会在括号后面放置特殊的注释。尽管这对于含有深度嵌套结构的长函数可能有意义, 但实际上应该做的是缩短函数。

public class wc{
    public static void main(String[] args) {
        BufferedReader in = new BufferedReader(new InputStreamReader(System.in));
        String line;
        int lineCount = 0;
        int charCount = 0;
        int wordCount = 0;
        try{
            while ((line = in.readLine()) != null) {
                lineCount++;
                charCount += line.length();
                String words[] = line.split("\\W");
                wordCount += words.length;
            } //while
            System.out.println("wordCount = " + wordCount);
            System.out.println("lineCount = " + lineCount);
            System.out.println("charCount = " + charCount);
        } // try
        catch (IOException e) {
            System.err.println("Error:" + e.getMessage());
        } //catch
    }//main
}
  • 归属与署名
/* Added by Mandy */

源代码控制系统非常善于记住是谁在何时添加了什么。尤其是注释在那儿放了一年又一年,越来越不准确,越来越和原作者没关系。

重申一下, git是这类信息最好的归属地。

  • 注释掉的代码

直接把代码注释掉是讨厌的做法。别这么干!

InputStreamResponse response = new InputStreamResponse();
response.setBody(formatter.getResultStream(), formatter.getByteCount());
// InputStream resultsStream = formatter.getResultStream();
// StreamReader reader = new StreamReader(resultsStream);
// response.setContent(reader.read(formatter.getByteCount()));

其他人不敢删除注释掉的代码。他们会想,代码依然放在那儿,一定有其原因, 而且这段代码很重要, 不能删除。注释掉的代码堆积在一起, 就像破酒瓶底的渣滓一般。

  • HTML注释

源代码注释中的 HTML 标记是一种厌物, 编辑器/IDE 中的代码本来易于阅读,却因为 HTML 注释的存在而变得难以卒读。如果注释将由某种工具(例如Javadoc)抽取出来, 呈现到网页, 那么该是工具而非程序员来负责给注释加上合适的 HTML 标签。

  • 非本地信息

假如你一定要写注释,请确保它描述了离它最近的代码。

以下例子,注释里写了端口号,但这段代码跟端口号有什么关系呢?以及如果端口号改变了,那这段注释是不是也的修改,有点属于脱裤子放屁了。

/*** Port on which fitnesse would run. Defaults to <b>8082</b>.
* @param fitnessePort
*/
public void setFitnessePort(int fitnessePort){
    this.fitnessePort = fitnessePort;
}
  • 信息过多

别在注释中添加有趣的历史性话题或者无关的细节描述。

  • 不明显的联系

注释及其描述的代码之间的联系应该显而易见。如果你不嫌麻烦要写注释, 至少让读者能看着注释和代码, 并且理解注释所谈何物。以来自 Apache 公共库的这段注释为例:

/** start with an array that is big enough to hold all the pixels
* (plus filter bytes), and an extra 200 bytes for header info*/
this.pngBytes = new byte[((this.width + 1) * this.height * 3) + 200];

过滤器字节是什么? 与那个+1 有关系吗? 或与*3 有关? 还是与两者皆有关? 为什么用200? 注释的作用是解释未能自行解释的代码。如果注释本身还需要解释, 就太遗憾了。

  • 函数头

短函数不需要太多描述。为只做一件事的短函数选个好名字,通常要比写函数头注释要好。

  • 非公共代码中的 Javadoc

虽然 Javadoc对于公共 API非常有用,但对于不打算作公共用途的代码就令人厌恶了。为系统中的类和函数生成Javadoc页并非总有用,而 Javadoc注释额外的形式要求几乎等同于八股文章。

格式
格式的目的

先明确一下,代码格式很重要。代码格式不可忽略,必须严肃对待。代码格式关乎沟通,而沟通是专业开发者的头等大事。 或许你认为“让代码能工作”才是专业开发者的头等大事。然而,我希望本书能让你抛掉那种想法。

垂直格式

源代码文件该有多大?在 Java中,文件尺寸与类尺寸极其相关。讨论类时再说类的尺寸。现在先考虑文件尺寸。多数 Java源代码文件有多大?事实说明,尺寸各各不同,长度殊异。

但短文件比长文件更易于理解。

向报纸学习

想想看写得很好的报纸文章。你从上到下阅读。在顶部,你期望有个头条, 告诉你故事主题,好让你决定是否要读下去。第一段是整个故事的大纲,给出粗线条概述, 但隐藏了故事细节。接着读下去,细节渐次增加,直至你了解所有的日期、名字、引语、说法及其他细节。

源文件也要像报纸文章那样。名称应当简单且一目了然。名称本身应该足够告诉我们是否在正确的模块中。源文件最顶部应该给出高层次概念和算法。细节应该往下渐次展开,直至找到源文件中最底层的函数和细节。

报纸由许多篇文章组成:多数短小精悍。有些稍微长点儿。很少有占满一整页的。这样做,报纸才可用。假若一份报纸只登载一篇长故事, 其中充斥毫无组织的事实、日期、名字等, 没人会去读它。

  • 8
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值