有意义的命名
软件中随处可见命名。我们给变量、函数、参数、类和封包命名。我们给源代码及源代码所在目录命名。我们给jar文件、war文件和ear文件命名。如下是一些优秀命名的建议。
名副其实
好的命名会体现出它为什么会存在、它做什么事、应该怎么用。如果名称需要注释来补充,就不算名副其实。
如下例,名称d没有体现出时间的感觉,更体现不出“以日统计”。
int d; // 消逝的时间,以日统计
可以替换为如下的变量声明
int elapsedTimeInDays;
int daysSinceCreation;
int daysSinceModification;
int fileAgeInDays;
再看下面的if判断,这里的数字1,无法看出它究竟代表什么意思,称为魔数。应该用有意义的名称代替。
if (status == 1) { … }//开关状态,1表示开,0表示关
可以替换为如下的常量
if(status == SWITCH_OPEN) { … }
避免误导
程序员必须避免留下掩藏代码本意的错误线索。应当避免使用与本意相悖的词。例如,accountList代表一组账号,不过它真的是List类型吗?如果不是请修改为accountGroup,否则会引起误导。
提防使用不同之处较小的名称,不要在同一“地点”使用在“外形”上难以区分的两个名称。例如下面的两个变量声明,需要花很长时间来进行区分。
XYZControllerForEffcientHandlingOfStrings
XYZControllerForEfficientStorageOfStrings
慎用小写字母l、大写字母O作为变量名,它们与数字1和0太相似了。
做有意义的区分
以数字系列命名(a1,a2,......aN)是依义命名的对立面。这样的名称纯属误导因为这样的命名完全没有提供正确信息,没有提供导向作者意图的线索。
void copyChars(char a1[], char a2[]) {…}
void copyChars(char source[], char destination[]) {…}
显然source、destination比a1、a2像样多了,这就是有意义的区分。不要做无意义的区分,ProductInfo或ProductData里面的Info和Data就是意义含混的废话,还不如直接用Product。Variable一词永远不要出现在变量名中,Table一词永远不要出现在表名中。废话都是冗余,要区分名称,就要以读者能鉴别出不同之处的方式来区分。
使用读的出来的名称
人类大脑擅长于记忆和使用单词。如果名称读不出来,讨论的时候就像个傻鸟。
Date genymdhms;
Date generationTimestamp;
genymdhms是什么意思?能读的出是“生成日期,年、月、日、时、分、秒”吗?读不出来的话,还是换成generationTimestamp(生成时间戳)更好点。
使用可搜索的名称
单字母名称和数字常量很难在一大篇文字中找出来,试想一下在整个工程源码中想通过搜索变量e来了解它的来龙去脉会是什么情况。一般单字母名称仅用于短方法中的本地变量或者循环计数器。名称长短应与其作用域大小相对应。若变量或常量可能在代码中多处使用,则应赋其以便于搜索的名称。如下例,类似s和4在代码中会经常出现,不便于搜索。
for (int j=0;j<34;j+){
s += (t[j]*4)/5;
}
int realDaysPerIdealDay = 4;
const int WORKS_DAY_PER_WEEK = 5;
for (int j=0;j<NUMBER_OF_TASKS;J++){
int realTaskDays = taskEstimate[j]*realDaysPerIdealDay;
int realTaskWeeks = realTaskDays/WORKS_DAY_PER_WEEK;
sum += realTaskWeeks;
}
避免使用编码
使用“编码规则”是自找麻烦,徒增了解代码的负担。Fortran语言要求首字母体现出类型。Basic早期版本只允许使用一个字母再加上一位数字。Java程序员不需要类型编码。对象是强类型的,代码编辑环境在编译开始前就已经侦测到类型错误。如下例,类型变化时,名称并不变化!
PhoneNumber phoneString;
不必用类似m_的前缀来标明成员变量。应当把类和函数做的足够小,消除对成员前缀的需要。此外人们应该只看到名称中有意义的部分,前缀是不入法眼的废料,变成了旧代码的标志物。
private String m_dsc;
类名和方法名
类名和对象名应该是名词或名词短语,如Customer、WikiPage、Account、AddressParser。类名不应该是动词。
方法名应该是动词或动词短语。如postPayment、deletePage、save。
属性访问器、修改器和断言应该根据其值命名,并加上get、set、is前缀。例如getName、setName、isPosted。
String name = employee.getName();
customer.setName(“mike”);
If(paycheck.isPosted()){......}
取名避免耍宝
耍宝的做法在代码中经常体现为使用俗语或者俚语。例如,别用whack()来表示kill(),别用eatMyShorts()这类与文化紧密相关的文化表示abort()。
每一个概念对应一个词
给每一个抽象概念选一个词,并且一直使用。不要混用fetch、retrieve、get这些意思相近的词。例如controller、manager,最好选择其中一个,并一直使用。
别用双关语
避免将同一单词用于不同目的,同一术语用于不用概念,基本上就是双关语了。例如add如果表示将通过增加或者连接两个现存值来获得新值,那么当表达将一个参数放到集群(collection)中这个意思时就不应该再用add,可以改用insert或者append之类词来命名。
代码作者应尽力写出易于理解的代码。我们要把代码写的让别人能一目了然,而不必殚精竭虑的研究。
使用解决方案领域名称
只有程序员才会读你的代码。所以,尽管用计算机科学术语、算法名、模式名、数学术语。依据问题所涉领域来命名不算是聪明的做法,因为不该让协作者老是跑去问客户每个名称的含义,其实他们早应该通过另一名称了解这个概念。
优秀的程序员和设计师,其工作之一就是分离解决方案领域和问题领域的概念。与所涉问题领域更为贴近的代码,应当采用源自问题领域的名称。
添加有意义的语境
很少有名称是能自我说明的——多数都不能。反之,你需要用有良好命名的类、函数或者名称空间来放置名称,给读者提供语境。如果没这么做,给名称添加前缀就是最后一招了。
假设firstName、lastName、street、houseNumber、city、state这些变量放在一起,则很明确的构成了一个地址。但如果只看见孤零零的state呢?你会理所当然的推断它是某个地址的一部分吗?这时最好添加前缀(如addrState),以此提供语境。必要的时候应该将这些变量和其相应的方法封装到一个类里面以增强语境。
不要添加没用的语境
只要短名称足够清楚,就要比长名称好,不要给名称添加不必要的语境。对于Address类的实体来说,accountAddress和customerAddress都是不错的名称,不过用在类名上就不太好了。Address是个类名。如果需要与MAC地址、端口地址和Web地址相区分,建议使用PostalAddress、MAC、URL这样的名称更为精确。精确正是命名的要点。
注释
什么也比不上放置良好的注释来的有用。什么也不会比乱七八糟的注释更有本事搞乱一个模块。什么也不会比陈旧、提供错误信息的注释更有破坏性。
注释的恰当用法是弥补我们在用代码表达意图时遭遇的失败。如果你发现自己需要写注释,再想想看是否有办法翻盘,用代码来表达。注释存在的越久,就离其所描述的代码越远,越来越变得全然错误。原因很简单,程序员不能坚持维护注释。代码在变动,演化。但是注释不一定。导致代码与注释演化的不同步,从而给程序员造成理解上的错误。我们要尽量减少注释量。将更多的精力放在代码表达力上。 代码能忠实告诉我们它做的事情。在给代码进行注释时,可以考虑如下几条建议。
注释不能美化糟糕的代码
与带有少量注释的整洁而有表达力的代码,要比带有大量注释的零碎而复杂的代码像样的多。其花时间为糟糕的代码注释,不如花时间清洁糟糕的代码。
好的注释
有些注释是必须的,也是有利的。如下是一些认为值得写的注释。不过要记住,唯一真正好的注释是你想办法不去写的注释。
- 法律信息
一般,公司代码规范要求编写与法律相关的注释。例如版权及著作权声明就是必须有理由在每个源文件开头注释处放置的内容。
//Copyright (C) 2003,2004,2005 by Object Mentor,Inc.All rights reserved.
//Released under the terms of the GNU General Pulic License version 2 or later.
- 提供信息的注释
用注释来提供基本的信息也有其用处。例如,用注释解释某个抽象方法的返回值;
//Returns an instance of the Responder being tested.
protected abstract Responder responderInstance();
这类注释有时管用,但是更好的方式是尽量利用函数名称传达信息。比如在上例中,只要把函数重命名为responderBeingTested,注释就是多余的了。
- 对于意图的解释
注释不仅体现了有关现实的有用信息【功能,用法等】,并且体现了某个决定后面的意图【目的】。
- 阐释
有时,注释把某些晦涩难懂的参数或返回值的意义解释为某种可读形式,也是会有用的。通常,更好的方法是尽量让参数或返回值自身就足够清楚;但如果参数或返回值是某个标准库的一部分,或是你不能修改的代码,使用阐释就会很有用。当然这也会冒着阐释性注释本身就不正确的风险。
- 警示
用于警告其他程序员会出现某种后果的注释也是很有用的。例如下面的注释绝对有道理存在,它能组织某位急切的程序员以效率之名使用静态初始器。
public static SimpleDateFormat makeStandardHttpDateFormat(){
//SimpleDateFormat is not thread safe,
//So we need to create each instance independently.
SimpleDateFormat df = new SimpleDateFormat(“EEE, dd MM yyyy HH:mm:ss z”);
df.setTimeZone(TimeZone.getTimeZone(“GMT”));
return df;
}
- TODO注释
用来在源码中放置要做的工作列表。(将来打算做什么?为什么还没做?)TODO注释是一种程序员认为应该做,但由于某种原因目前还没做的工作。它可能是要提醒删除某个不必要的特性,或者要求他人注意某个问题。它可能是恳求别人取个好名字,或者提示对依赖于某个计划事件的修改。无论TODO的目的何在,他都不是在系统中留下糟糕代码的接口。
- 公共API中的Javadoc
没有什么比被良好描述的公共API更有用和令人满意的了。标准java库中的javadoc是好注释的例子。如果编写公共API,就该为它编写良好的Javadoc。
坏的注释
通常,坏注释都是糟糕的代码的支撑或者借口,或者对错误决策的修正。
- 喃喃自语
如果你决定写注释,就要花必要的时间确保写出最好的注释。如果作者喃喃自语,表达不清楚自己的想法(对于作者来说可能有意义),对于后续工作者来说将是一个谜团。
- 多余的注释
注释若不能比代码本身提供更多的信息,读注释并不比读代码容易,那它就是多余的的注释。多余的注释有可能不如代码精确,甚至可能误导读者。
- 误导性注释
不精确的注释有误导作用。所以代码的注释,一定要反复斟酌。
- 循规式注释
要求每个函数都有javadoc , 每个变量都有注释的规矩是愚蠢可笑的。这种注释让代码变的散乱,不易让人理解。
- 日志式注释
每次编辑代码,都在模块开始处添加一条注释,这类注释就像是一种记录每次修改的日志。因为版本控制器的存在,应该抛弃这种方式。
- 废话注释
如下例,这种注释纯属废话,我们都应该视而不见。读代码时,眼光不会停留在上面。最终当代码修改后,这类注释就变作了谎言一堆。
/**
* Return the days of the month
*
*/
public int getDayofMonth(){
Return dayofMonth;
}
- 能用函数或者变量时就不要注释
如果能用函数名、变量名、代码解释的注释就尽量使用代码来解释。
- 括号后面的注释
有时程序员会在for循环、while循环的右括号后面放置特殊的注释。对于含有深度嵌套结构的长函数可能有意义,但是只会给更愿意编写短小、封装的函数带来混乱。如果你发现自己想标记右括号,其实因该做的是缩短函数。
- 注释掉的代码
直接把代码注释掉很让人恶心,造成混乱。这样会导致后来的代码维护者不敢删除注释掉的代码。他们会想,代码依然放在那儿,一定有其原因,而且这段代码很重要,不能删除。注释掉的代码堆积在一起,就像破酒瓶底的渣滓一样。我们已经拥有优良的源代码控制系统,无需用注释来标记无用的代码。
- 信息过多
代码中注释的信息,应该是简单的,有针对性的。不要在注释中讲故事,以及一些不相关的事情。
- 不明显的联系
注释和代码之间的联系,应该是显而易见的。如果你不嫌麻烦要写注释,至少让读者能看着注释和代码,并且理解注释所谈何物。
- 函数头
短函数不需要太多描述。为只做一件事的短函数选个好名字,通常要比写函数头注释要好。
- 非公共代码中的Javadoc
虽然Javadoc对于公共API非常有用,但对于不打算作公共用途的代码就令人厌恶了。为系统中的类和函数生成Javadoc页并非总有用,而Javadoc注释额外的形式要求几乎等同于八股文章。
格式
我们应该保持良好的代码格式,应该选用一套管理代码格式的简单规则,然后贯彻这些规则。如果我们在团队中工作,则团队应该采用一套简单的格式规则,所有成员都要遵从。使用应用格式化规则的自动化工具会很有帮助。
代码格式关乎沟通,而沟通是专业开发者的头等大事。或许认为“让代码能工作”才是专业开发者的第一要务,但是今天编写的功能,极有可能在下一版本中被修改,但代码的可读性却会对以后可能发生的修改行为产生深远影响。原始代码修改之后很久,其代码风格和可读性仍会影响到可维护性和扩展性。即便代码已不复存在,你的风格和律条仍存活下来。以下是一些可以帮忙我们更好沟通的代码格式。
垂直格式
- 向报纸学习
看看写得很好的报纸。从上到下阅读,在顶部,有头条,告诉你故事主题。然后第一段是大纲,接着细节渐次增加。
源代码应当一样,名称简单且一目了然。源文件最顶部应该给出高层次概念和算法。细节应该往下渐次展开,直到最底层的函数和细节。
- 概念间垂直方向上的区隔
每一行代码展现一个表达式或者一个子句,每一组代码行展示一条完整的思路,这些思路需要用空白行区隔开来。
如下例,在封包声明、导入声明和每个函数之间,都有空白行隔开。每个空白行都是一条线索,标识出新的独立的概年。
package fitnesse.wikitext.widgets;
import java.util.regex.*;
public class BoldWidget extends ParentWidget{
public static final String REGEXP = “’’’.+?’’’”;
private static final Pattern pattern = Pattern.compile(“’’’’(.+?)’’’”,Pattern.MULTILINE + Pattern.DOTALL\);
Public BoldWidget(…) throws Exception {
...
}
}
- 垂直方向上的靠近
正因为空白行隔开了概念,靠近的代码行则暗示了它们之间的紧密关系。这有助于对代码的理解。正如上面的例子,对于BoldWidget类,一眼就能看出有两个变量一个方法。
- 垂直距离
关系密切的概念应该互相靠近。应避免迫使读者在源文件和类中跳来跳去。
变量声明尽可能靠近其使用位置。因为函数很短,本地变量应该在函数的顶部出现。循环中的控制变量应该总是在循环语句中声明。
实体变量应该在类的顶部声明。这样不会增加变量的垂直距离。因为在设计良好的类中,它们不是被该类的所有方法也是被大多数方法使用。
相关函数 若某个函数调用了另外一个,就应该把他们放在一起,而且调用者应该尽可能放在被调用者上方。
- 垂直顺序
一般来说,要自上而下展示函数的调用依赖顺序。被调用的函数应该放在执行调用的函数下面,建立一种自顶向下贯穿的良好信息流。像读文章一样,最重要的概念先出来,最底层的细节最后出来。从最前面的几个函数获知要旨,而不至于迷失于细节中。
横向格式
- 水平方向的区隔与靠近
我们使用空格字符将彼此紧密联系相关的事务联系在一起,也使用空格字符把相关性较弱的事物分隔开。如下例,在赋值操作符周围加上空格字符,以此达到强调目的。赋值语句有两个确定而重要的要素:左边和右边。空格字符增强了分隔效果。
int lineSize = line.length();
lineWidthHistogram.addLine(lineSize, lineCount);
- 缩进
我们依照源代码行在继承结构中的位置对源代码行做缩进处理。在文件顶层的语句,例如大多数的类声明,根本不缩进。类中的方法相对该类缩进一个层级。方法的实现相对方法声明缩进一个层级,代码块的实现相对于其容器代码块缩进一个层级,以此类推。
程序员相当依赖缩进模式来看清楚自己在什么范围工作,这样可以快速跳过与当前关注的情形无关的范围。
- 空范围
有时,while或for语句的语句体为空,如下所示。,尽量不要使用,如果无法避免,就确保空范围体的缩进,用括号包围起来。
while(dis.read(buf,0,readBufferSize)!=-1);
团队规则
每个程序员都有自己喜欢的格式规则,但是在团队中,规则由团队说了算。好的软件系统是由一系列读起来不错的代码文件组成的,它们需要一致和顺畅的风格。不同的风格,会增加代码阅读和维护的复杂度。
错误处理
错误处理只不过是编程时必须要做的事之一。输入可能出现异常,设备可能失效。简言之,可能会出现错误,当错误发生时,程序员就有责任确保代码照常工作。然而,应该弄清楚错误处理与整洁代码的关系。许多程序充斥着凌乱的错误处理代码。错误处理很重要,但如果它搞乱了代码逻辑,就是错误的做法。如下为编写既整洁又强固的代码—雅致的处理错误代码的技巧和思路。
- 使用异常而非返回码
很久以前,许多语言都不支持异常。这些语言处理和汇报错误的手段有限。要么设置一个错误标识,要么返回给调用者检查的错误码。调用者必须在调用之后即可检查错误。不幸的是,这个步骤很容易被遗忘。所以,遇到错误时,最好抛出一个异常。调用代码很整洁,其逻辑不会被错误处理搞乱。
- 先写Try-Catch-Finally语句
在某种意义上,try代码块就像是事务。Catch代码块将程序维持在一种持续状态,无论try代码块中发生了什么错误均如此。所以在编写可能抛出异常的代码时,最好先写出try-catch-finally语句。
- 给出异常发生的环境说明
抛出的每个异常,都应该提供足够的环境说明,以便判断错误的来源和处所。
Java中,可以从任何异常那里得到堆栈踪迹(stack trace),然而,堆栈踪迹却无法告诉你该失败操作的初衷。应当创建信息充分的错误消息(包括失败的操作和失败类型),并和异常一起传递出去。如果有日志系统,可以传递足够的信息给catch块,并记录下来。
- 依调用者需要定义异常类
错误分类有很多种方式。可以依其来源分类:是来自组件还是其他地方?或依其类型分类:是设备错误、网络错误还是编程错误?不过,我们在应用程序定义异常类时,最重要的考虑应该是他们如何被捕获。
- 别返回null值
错误处理中,容易引发错误的做法,第一个就是返回null值。 很多代码都在检查null值。 所以返回null值,基本上是在给自己增加工作量,也是在给调用者添乱。只要有一处没检查null值,应用程序就会失控。(在运行时得到一个NullPointerException异常)。
- 别传递null值
在方法中返回null值是糟糕的做法,但将null值传递给其他方法就更 糟糕了。判断是否为null。 在大多数编程语言中,没有良好的方法能对付由调用者意外传入的null值。恰当的做法是,禁止传入null值。