《代码整洁之道》笔记1

可紊 提交于 2020-02-21 17:29:32
1. 要有代码
代码确然是我们最终用来表达需求的那种语言。我们可以创造各种与需求接近 的语言。我们可以创造帮助把需求解析和汇整为正式结构的各种工具。然而,我们永远无法抛弃必要的精确性——所以代码永存。
勒布朗(LeBlanc)法 则:稍后等于永不(Later equals never)。
沼泽(wading):糟糕的代码,对代码的每次修改都影响到其他两三处代 码。

2.2 名副其实
变量、函数或类的名称应该已经答复了所有的大问题。它该告诉你,它为什么会存 在,它做什么事,应该怎么用。
如果名称需要注释来补充,那就不算是名副其实。
 int d; // 消逝的时间,以日计 
名称d什么也没说明。它没有引起对时间消逝的感觉,更别说以日计了。我们应该选 择指明了计量对象和计量单位的名称:
int elapsedTimeInDays;
int daysSinceCreation; 
int daysSinceModification; 
int fileAgeInDays; 

2.3 避免误导
别用accountList来指称一组账号,除非它真的是List类型。
List一词对程序员有特殊意 义。如果包纳账号的容器并非真是个List,就会引起错误的判断。
所以,用 accountGroup或bunchOfAccounts,甚至直接用accounts都会好一些。 
误导性名称真正可怕的例子,是用小写字母 l 和大写字母 O 作为变量名,尤其是在组合 使用的时候。当然,问题在于它们看起来完全像是常量“壹”和“零”。

2.4 做有意义的区分
 以数字系列命名(a1、a2,……aN)是依义命名的对立面。这样的名称纯属误导 ——完全没有提供正确信息;没有提供导向作者意图的线索。
试看: 
public static void copyChars(char a1[], char a2[]) 
{
     for (int i = 0; i < a1.length; i++) 
     { 
         a2[i] = a1[i]; 
     } 
 } 

如果参数名改为source 和 destination,这个函数就会像样许多。
废话是另一种没意义的区分。假设你有一个 Product 类。如果还有一个 ProductInfo 或 ProductData类,那它们的名称虽然不同,意思却无区别。Info和Data就像a、an和the一 样,是意义含混的废话。
废话都是冗余。Variable一词永远不应当出现在变量名中。Table一词永远不应当出现 在表名中。NameString会比Name好吗?难道Name会是一个浮点数不成?如果是这样,就 触犯了关于误导的规则。设想有个名为Customer的类,还有一个名为CustomerObject的 类。区别何在呢?

2.5 使用读得出来的名称
人类长于记忆和使用单词。大脑的相当一部分就是用来容纳和处理单词的。单词能读 得出来。人类进化到大脑中有那么大的一块地方用来处理言语,若不善加利用,实在是种 耻辱。
如果名称读不出来,讨论的时候就会像个傻鸟。

class DtaRcrd102 { 
    private Date genymdhms; //(生成日期,年、月、日、时、分、秒
    private Date modymdhms; 
    private final String pszqint = "102"; 
    /* ... */ 
}; 
 class Customer { 
     private Date generationTimestamp;
     private Date modificationTimestamp;
    private final String recordId = "102"; 
    /* ... */ 
}; 


2.6 使用可搜索的名称
单字母名称和数字常量有个问题,就是很难在一大篇文字中找出来。 找MAX_CLASSES_PER_STUDENT很容易,但想找数字7就麻烦了,它可能是某些文 件名或其他常量定义的一部分,出现在因不同意图而采用的各种表达式中。
如果该常量是 个长数字,又被人错改过,就会逃过搜索,从而造成错误。
同样,e也不是个便于搜索的好变量名。
对比:
for (int j=0; j<34; j++) 
{ 
    s += (t[j]*4)/5;
} 

和
int realDaysPerIdealDay = 4; 
const int WORK_DAYS_PER_WEEK = 5; 
int sum = 0; 
for (int j=0; j < NUMBER_OF_TASKS; j++) 
{ 
    int realTaskDays = taskEstimate[j] * realDaysPerIdealDay; 
    int realTaskWeeks = (realdays / WORK_DAYS_PER_WEEK); 
    sum += realTaskWeeks;
} 


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

2.10 方法名
方法名应当是动词或动词短语,如postPayment、deletePage或save。属性访问器、修 改器和断言应该根据其值命名,并依Javabean标准[10]加上get、set和is前缀.
string name = employee.getName(); 
customer.setName("mike"); 
if (paycheck.isPosted())... 
重载构造器时,使用描述了参数的静态工厂方法名。例如,
 Complex fulcrumPoint = Complex.FromRealNumber(23.0); 
通常好于 Complex fulcrumPoint = new Complex(23.0); 
可以考虑将相应的构造器设置为private,强制使用这种命名手段

2.16 添加有意义的语境
很少有名称是能自我说明的——多数都不能。反之,你需要用有良好命名的类、函数 或名称空间来放置名称,给读者提供语境。如果没这么做,给名称添加前缀就是最后一招 了。 

设想你有名为firstName、lastName、street、houseNumber、city、state和zipcode的变 量。当它们搁一块儿的时候,很明确是构成了一个地址。
不过,假使只是在某个方法中看 见孤零零一个state变量呢?你会理所当然推断那是某个地址的一部分吗? 
可以添加前缀addrFirstName、addrLastName、addrState等,以此提供语境。至少,读 者会明白这些变量是某个更大结构的一部分。
当然,更好的方案是创建名为Address的 类。这样,即便是编译器也会知道这些变量隶属某个更大的概念了。 

3.2 一个方法只做一件事
函数应该做一件事。做好这件事。只做这一件事

3.4 switch语句
写出短小的switch语句很难[6]。即便是只有两种条件的switch语句也要比我想要的单 个代码块或函数大得多。写出只做一件事的switch语句也很难。Switch天生要做N件事。 不幸我们总无法避开switch语句,不过还是能够确保每个switch都埋藏在较低的抽象层 级,而且永远不重复。当然,我们利用多态来实现这一点。
使用简单工厂和State模式替换冗余的 switch

3.9 抽离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函数只与错误处理有关。很容易理解然后就忽略掉。
 deletePageAndAllReference函数只与完全删除一个page有关。
错误处理可以忽略掉。有了 这样美妙的区隔,代码就更易于理解和修改了。

4.1 注释不能美化糟糕的代码
写注释的常见动机之一是糟糕的代码的存在。我们编写一个模块,发现它令人困扰、 乱七八糟。我们知道,它烂透了。我们告诉自己:“喔,最好写点注释!”不!最好是把代 码弄干净!
 带有少量注释的整洁而有表达力的代码,要比带有大量注释的零碎而复杂的代码像样 得多。与其花时间编写解释你搞出的糟糕的代码的注释,不如花时间清洁那堆糟糕的代 码

4.2 用代码来阐述
有时,代码本身不足以解释其行为。不幸的是,许多程序员据此以为代码很少——如 果有的话——能做好解释工作。这种观点纯属错误。你愿意看到这个: 
// Check to see if the employee is eligible for full benefits 
if ((employee.flags & HOURLY_FLAG) && (employee.age > 65)) 
//还是这个? 
if (employee.isEligibleForFullBenefits()) 

只要想上那么几秒钟,就能用代码解释你大部分的意图。很多时候,简单到只需要创 建一个描述与注释所言同一事物的函数即可。

4.3 好注释
有些注释是必须的,也是有利的。来看看一些我认为值得写的注释。不过要记住,唯 一真正好的注释是你想办法不去写的注释。

4.3.1 法律信息
有时,公司代码规范要求编写与法律有关的注释。例如,版权及著作权声明就是必须 和有理由在每个源文件开头注释处放置的内容。 
下例是我们在FitNesse 项目每个源文件开头放置的标准注释。我可以很开心地说, IDE自动卷起这些注释,这样就不会显得凌乱了。
 // 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. 
这类注释不应是合同或法典。只要有可能,就指向一份标准许可或其他外部文档,而 不要把所有条款放到注释中。

4.3.2 提供信息的注释
有时,用注释来提供基本信息也有其用处。
例如,以下注释解释了某个抽象方法的返 回值: 
// Returns an instance of the Responder being tested. 
protected abstract Responder responderInstance(); 
这类注释有时管用,但更好的方式是尽量利用函数名称传达信息。
比如,在本例中, 只要把函数重新命名为responderBeingTested,注释就是多余的了

4.3.3 对意图的解释
有时,注释不仅提供了有关实现的有用信息,而且还提供了某个决定后面的意图。
在 下例中,我们看到注释反映出来的一个有趣决定。在对比两个对象时,作者决定将他的类 放置在比其他东西更高的位置。 
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. 
} 


4.3.4 阐释
有时,注释把某些晦涩难明的参数或返回值的意义翻译为某种可读形式,也会是有用 的。通常,更好的方法是尽量让参数或返回值自身就足够清楚;但如果参数或返回值是某 个标准库的一部分,或是你不能修改的代码,帮助阐释其含义的代码就会有用。 
public void testCompareTo() throws Exception 
{ 
    WikiPagePath a = PathParser.parse("PageA"); 
    WikiPagePath b = PathParser.parse("PageB"); 
    assertTrue(a.compareTo(a) == 0); // a == a 
    assertTrue(a.compareTo(b) != 0); // a != b 
 } 


4.3.5 警示
有时,用于警告其他程序员会出现某种后果的注释也是有用的。
例如,下面的注释解 释了为什么要关闭某个特定的测试用例: 
// 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); 
}

这里有个更麻烦的例子:
public static SimpleDateFormat makeStandardHttpDateFormat()
{
     //SimpleDateFormat is not thread safe,
     //so we need to create each instance independently. 
     SimpleDateFormat df = new SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss z");
     df.setTimeZone(TimeZone.getTimeZone("GMT")); return df;
}

你也许会抱怨说,还会有更好的解决方法。我大概会同意。不过上面的注释绝对有道 理存在,它能阻止某位急切的程序员以效率之名使用静态初始器。


4.3.6 TODO注释
有时,有理由用//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; } 

 

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!