《代码整洁之道》笔记1
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; }