“代码作者应尽力写出易于理解的代码。我们想把代码写得让别人能一目尽览,而不比殚精竭虑地研究。我们想要那种大众化的作者尽责写清楚的平装书模式;我们不想要那种学者挖地三尺才能明白个中意义的学院派模式。” ——《代码整洁之道》Robert C.Martin 著 韩磊 译
目录
命名规则一:名副其实
命名规则二:避免误导
命名规则三:做有意义的区分
命名规则四:使用读得出来的名称
命名规则五:使用可搜索的名称
命名规则六:避免使用编码
命名规则七:避免思维映射
命名规则八:类名
命名规则九:方法名
命名规则十:每个概念对应一个词
命名规则十一:别用双关语
命名规则十二:使用解决方案领域名称
命名规则十三:使用源自所涉问题领域的名称
命名规则十四:添加有意义的语境
命名规则十五:不要添加没用的语境
变量、函数或类的名称应该已经答复了所有的大问题。它该告诉你,它为什么会存在,它做什么事,应该怎么用。如果名称需要注释来补充,那就不算是名副其实。
一旦发现更好的名称,就换掉旧的名称。
例如:int d; // 消逝的时间,以日计
名称d什么也没说明。它没有引起对时间消逝的感觉,更别说以日计了。我们应该选择指明了计量对象和计量单位的名称:
int elapsedTimeInDays;
int daysSinceCreation;
int daysSinceModification;
int fileAgeInDays;
别用accountList来指称一组账号,除非它真的是List类型。List一词对程序员有特殊意义。如果包纳账号的容器并非真是个List,就会引起错误的判断(即便容器就是个List,最好也别在名称中写出容器类型名)。所以,用accountGroup或bunchOfAccounts,甚至直接用accounts都会好一些。
提防使用不同之处较小的名称。想区分模块中某处的XYZControllerForEfficientHandlingOfStrings和另一处的XYZControllerForEfficientStorageOfStrings
误导性名称真正可怕的例子,是用小写字母l和大写字母O作为变量名,尤其是在组合使用的时候。因为问题在于它们看起来完全像是常量“壹”和“零”。
例如,因为同一作用范围内两样不同的东西不能重名,你可能会随手改掉其中一个的名称。有时干脆以错误的拼写充数,结果就是出现在更正拼写错误后导致编译器出错的情况。
添加数字系列:以数字系列命名(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一词永远不应当出现在表名中。设想有个Customer的类,还有一个名为CustomerObject的类。区别何在呢?
getActiveAccount();
getActiveAccounts();
getActiveAccountInfo();
程序员怎么能知道该调用哪个函数呢?要区分名称,就要以读者能鉴别不同之处的方式来区分。
比较
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";
/* ... */
};
单字母名称和数字常量有个问题,就是很难在一大篇文字中找出来。
找MAX_CLASSES_PER_STUDENT很容易,但想找数字7就麻烦了,它可能是某些文件名或其他常量定义的一部分,出现在因不同意图而采用的各种表达式中。如果该常量是个长数字,又被人错改过,就会逃过搜索,从而造成错误。
若变量或常量可能在代码中多处使用,则应赋其以便于搜索的名称。比较
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 = (realTaskDays / WORK_DAYS_PER_WEEK);
sum += realTaskWeeks;
}
匈牙利语标记法(Hungarian Notation,HN) 在Windows的C语言API的时代,HN相当重要,那时所有名称要么是个整数句柄,要么是个长指针或者void指针,要不然就是string的几种实现(有不同的用途和属性)之一。那时候编译器并不做类型检查,程序员需要匈牙利语标记法来帮助自己记住类型。
现代编程语言具有更丰富的类型系统,编译器也记得并强制使用类型。而且,人们趋向于使用更小的类、更短的方法,好让每个变量的定义都在视野范围之内。所以,如今HN和其他类型编码形式都纯属多余。它们增加了修改变量、函数或类的名称或类型的难度。它们增加了阅读代码的难度。它们制造了让编码系统误导读者的可能性。
成员前缀 也不必用 m_前缀来标明成员变量。应当把类和函数做得足够小,消除对成员前缀的需要。你应当使用某种可以高亮或用颜色标出成员的编辑环境。
public class Part {
private String m_dsc; // The textual description
void setName(String name) {
m_dsc = name;
}
}
--------------------------------------------------------------------------------------
public class Part {
String description;
void setDescription(String description) {
this.description = description;
}
}
接口与实现 比如,你在做一个创建形状用的抽象工厂(Abstract Factory)。该工厂是个接口,要用具体类来实现。你怎么来命名工厂和具体类呢?IShapeFactory和ShapeFactory吗?我喜欢不加修饰的接口。前导字母I被滥用到了说好听点是干扰,说难听点根本就是废话的程度。我不想让用户知道我给他们的是接口。我就想让他们知道那是个ShapeFactory。如果接口和实现必须选一个来编码的话,我宁肯选择实现。ShapeFactoryImp,甚至是丑陋的CShapeFactory,都比对接口名称编码来得好。
不应当让读者在脑中把你的名称翻译为他们熟知的名称。这种问题经常出现在选择是使用问题领域术语还是解决方案领域术语时。
单字母变量名就是个问题。在作用域较小、也没有名称冲突时,循环计数器自然有可能被命名为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("mike");
if (paycheck.isPosted())...
重载构造器时,使用描述了参数的静态工厂方法名。例如,
Complex fulcrumPoint = Complex.FromRealNumber(23.0);
通常好于
Complex fulcrumPoint = new Complex(23.0);
可以考虑将相应的构造器设置为private,强制使用这种命名手段。
给每个抽象概念选一个词,并且一以贯之。例如:在同一堆代码中有controller,又有manager,还有driver,就会令人困惑。DeviceManager和Protocol-Controller 之间有何根本区别?为什么不全用 controllers 或 managers?他们都是Drivers吗?这种名称,让人觉得这两个对象是不同类型的,也分属不同的类。
比如,在多个类中都有add方法,该方法通过增加或连接两个现存值来获得新值。假设要写个新类,该类中有一个方法,把单个参数放到群集(collection)中。该把这个方法叫做 add 吗?这样做貌似和其他 add 方法保持了一致,但实际上语义却不同,应该用 insert或append之类词来命名才对。把该方法命名为add,就是双关语了。
只有程序员才会读你的代码。所以,尽管用那些计算机科学(Computer Science,CS)术语、算法名、模式名、数学术语吧。依据问题所涉领域来命名可不算是聪明的做法,因为不该让协作者老是跑去问客户每个名称的含义,其实他们早该通过另一名称了解这个概念了。
如果不能用程序员熟悉的术语来给手头的工作命名,就采用从所涉问题领域而来的名称吧。至少,负责维护代码的程序员就能去请教领域专家了。
优秀的程序员和设计师,其工作之一就是分离解决方案领域和问题领域的概念。与所涉问题领域更为贴近的代码,应当采用源自问题领域的名称。
看看代码清单2-1中的方法。以下变量是否需要更有意义的语境呢?函数名仅给出了部分语境;算法提供了剩下的部分。遍览函数后,你会知道number、verb和pluralModifier这三个变量是“测估”信息的一部分。不幸的是这语境得靠读者推断出来。第一眼看到这个方法时,这些变量的含义完全不清楚。
代码清单2-1 语境不明确的变量
private void printGuessStatistics(char candidate, int count) {
String number;
String verb;
String pluralModifier;
if (count == 0) {
number = "no";
verb = "are";
pluralModifier = "s";
} else if (count == 1) {
number = "1";
verb = "is";
pluralModifier = "";
} else {
number = Integer.toString(count);
verb = "are";
pluralModifier = "s";
}
String guessMessage = String.format(
"There %s %s %s%s", verb, number, candidate, pluralModifier
);
print(guessMessage);
}
上列函数有点儿过长,变量的使用贯穿始终。要分解这个函数,需要创建一个名为GuessStatisticsMessage的类,把三个变量做成该类的成员字段。这样它们就在定义上变作了GuessStatisticsMessage的一部分。语境的增强也让算法能够通过分解为更小的函数而变得更为干净利落。(如代码清单2-2所示。)
代码清单2-2 有语境的变量
public class GuessStatisticsMessage {
private String number;
private String verb;
private String pluralModifier;
public String make(char candidate, int count) {
createPluralDependentMessageParts(count);
return String.format("There %s %s %s%s", verb, number, candidate, pluralModifier );
}
private void createPluralDependentMessageParts(int count) {
if (count == 0) {
thereAreNoLetters();
} else if (count == 1) {
thereIsOneLetter();
} else {
thereAreManyLetters(count);
}
}
private void thereAreManyLetters(int count) {
number = Integer.toString(count);
verb = "are";
pluralModifier = "s";
}
private void thereIsOneLetter() {
number = "1";
verb = "is";
pluralModifier = "";
}
private void thereAreNoLetters() {
number = "no";
verb = "are";
pluralModifier = "s";
}
}
设若有一个名为“加油站豪华版”(Gas Station Deluxe)的应用,在其中给每个类添加GSD前缀就不是什么好点子。说白了,你是在和自己在用的工具过不去。输入G,按下自动完成键,结果会得到系统中全部类的列表,列表恨不得有一英里那么长。这样做聪明吗?为什么要搞得IDE没法帮助你?
再比如,你在GSD应用程序中的记账模块创建了一个表示邮件地址的类,然后给该类命名为GSDAccountAddress。稍后,你的客户联络应用中需要用到邮件地址,你会用GSDAccountAddress吗?这名字听起来没问题吗?在这17个字母里面,有10个字母纯属多余和与当前语境毫无关联。
只要短名称足够清楚,就要比长名称好。别给名称添加不必要的语境。
对于Address类的实体来说,accountAddress和customerAddress都是不错的名称,不过用在类名上就不太好了。Address是个好类名。如果需要与MAC地址、端口地址和Web地址相区别,我会考虑使用PostalAddress、MAC和URI。这样的名称更为精确,而精确正是命名的要点。