文章目录
第二章 有意义的命名
2.1 名副其实
选个好名字要花时间,但省下来的时间比花掉的多。注意命名,而且一旦发现有更好的名称,就换掉旧的。这么做,读你代码的人(包括你自己)都会更开心。
变量、函数或类的名称应该已经答复了所有的大问题。它该告诉你,它为什么会存在,它做什么事,应该怎么用。如果名称需要注释来补充,那就不算是名副其实。
int d; // 消逝的时间,以日计
/* 名称 d 什么也没说明。
* 它没有引起对时间消逝的感觉,更别说以日计了。
* 我们应该选择指明了计量对象和计量单位的名称.
* */
int elapsedTimeInDays;
int daysSinceCreation;
int daysSinceModification;
int fileAgeInDays;
选择体现本意的名称能让人更容易理解和修改代码。
public List<int[]> getThem() {
List<int[]> list1 = new ArrayList<int[]>();
for (int[] x : theList)
if (x[0] == 4)
list1.add(x);
return list1;
}
// 代码的模糊度:即上下文在代码中未被明确体现的程度。
// 通过好的命名降低代码的模糊度
// 只要改为有意义的名称,代码就会得到相当程度的改进
public List<int[]> getFlaggedCells() {
List<int[]> flaggedCells = new ArrayList<int[]>();
for (int[] cell : gameBoard)
if (cell[STATUS_VALUE] == FLAGGED)
flaggedCells.add(cell);
return flaggedCells;
}
// 不用 int 数组表示单元格,而是另写一个类。
// 该类包括一个名副其实的函数(称为isFlagged),从而掩盖住魔术数。
public List<Cell> getFlaggedCells() {
List<Cell> flaggedCells = new ArrayList<Cell>();
for (Cell cell : gameBoard)
if (cell.isFlagged())
flaggedCells.add(cell);
return flaggedCells;
}
只要简单改一下名称,就能轻易知道发生了什么。这就是选用好名称的力量。
2.2 避免误导
- 程序员必须避免留下掩藏代码本意的错误线索。应当避免使用与本意相悖的词。例如,
hp
、aix
和sco
都不该用做变量名,因为它们都是UNIX
平台或类UNIX
平台的专有名称。 - 别用
accountList
来指称一组账号,除非它真的是List
类型。List
一词对程序员有特殊意义。如果包纳账号的容器并非真是个List
,就会引起错误的判断。所以,用accountGroup
或bunchOfAccounts
,甚至直接用accounts
都会好一些。 - 提防使用不同之处较小的名称。想区分模块中某处的
XYZControllerFor EfficientHandlingOfStrings
和另一处的XYZControllerForEfficientStorageOfStrings
,会花多长时间呢?这两个词外形实在太相似了。以同样的方式拼写出同样的概念才是信息。拼写前后不一致就是误导。键入某个名称的前几个字母,按一下某个热键组合(如果有的话),就能得到一列该名称的可能形式。假如相似的名称依字母顺序放在一起,且差异很明显,那就会相当有助益,因为程序员多半会压根不看你的详细注释,甚至不看该类的方法列表就直接看名字挑一个对象。 - 误导性名称真正可怕的例子,是用小写字母
l
和大写字母O
作为变量名,尤其是在组合使用的时候。当然,问题在于它们看起来完全像是常量“壹”和“零”。
2.3 做有意义的区分
- 如果程序员只是为满足编译器或解释器的需要而写代码,就会制造麻烦。例如,因为同一作用范围内两样不同的东西不能重名,你可能会随手改掉其中一个的名称。有时干脆以错误的拼写充数,结果就是出现在更正拼写错误后导致编译器出错的情况。光是添加数字系列或是废话远远不够,即便这足以让编译器满意。如果名称必须相异,那其意思也应该不同才对。
- 以数字系列命名(
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,这个函数就会像样许多。
public static void copyChars(char source[], char destination[]) {
for (int i = 0; i < source.length; i++) {
destination[i] = source[i];
}
}
- 废话是另一种没意义的区分。假设你有一个
Product
类。如果还有一个ProductInfo
或ProductData
类,那它们的名称虽然不同,意思却无区别。Info
和Data
就像a
、an
和the
一样,是意义含混的废话。 - 注意,只要体现出有意义的区分,使用
a
和the
这样的前缀就没错。例如,你可能把a
用在域内变量,而把the
用于函数参数 - 废话都是冗余。
Variable
一词永远不应当出现在变量名中。Table
一词永远不应当出现在表名中。NameString
会比Name
好吗?难道Name
会是一个浮点数不成?如果是这样,就触犯了关于误导的规则。设想有个名为Customer
的类,还有一个名为CustomerObject
的类。区别何在呢?哪一个是表示客户历史支付情况的最佳途径?
// 程序员怎么能知道该调用哪个函数呢?
getActiveAccount();
getActiveAccounts();
getActiveAccountInfo();
- 如果缺少明确约定,变量 moneyAmount 就与 money 没区别,customerInfo 与 customer没区别,accountData与account没区别,theMessage也与message没区别。要区分名称,就要以读者能鉴别不同之处的方式来区分。
2.4 使用读得出来的名称
- 人类长于记忆和使用单词。大脑的相当一部分就是用来容纳和处理单词的。单词能读得出来。人类进化到大脑中有那么大的一块地方用来处理言语,应善加利用。
- 如果名称读不出来,讨论的时候就会像个傻鸟。“哎,这儿,鼻涕阿三喜摁踢(bee cee arr three cee enn tee)上头,有个皮挨死极翘(pee ess zee kyew)整数,看见没?”。这不是小事,因为编程本就是一种社会活动。
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.5 使用可搜索的名称
- 单字母名称和数字常量有个问题,就是很难在一大篇文字中找出来。找
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;
}
注意,上面代码中的sum并非特别有用的名称,不过它至少搜得到。采用能表达意图的名称,貌似拉长了函数代码,但要想想看,WORK_DAYS_PER_WEEK要比数字5好找得多,而列表中也只剩下了体现作者意图的名称。
2.6 避免使用编码
把类型或作用域编进名称里面,徒然增加了解码的负担。没理由要求每位新人都在弄清要应付的代码之外(那算是正常的),还要再搞懂另一种编码“语言”。这对于解决问题而言,纯属多余的负担。带编码的名称通常也不便发音,容易打错。
- 匈牙利语标记法。早期 IDE 不做类型检查,需要程序员使用匈牙利语标记法来帮助自己记住类型。
- 成员前缀。也不必用 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
,都比对接口名称编码来得好。
2.7 避免思维映射
不应当让读者在脑中把你的名称翻译为他们熟知的名称。这种问题经常出现在选择是使用问题领域术语还是解决方案领域术语时。明确是王道,编写其他人能理解的代码。
2.8 类名
类名和对象名应该是名词或名词短语,如Customer
、WikiPage
、Account
和AddressParser
。避免使用Manager
、Processor
、Data
或Info
这样的类名。类名不应当是动词。
2.9 方法名
方法名应当是动词或动词短语,如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,强制使用这种命名手段。
2.10 别扮可爱
不要使用俗语以及俚语等
2.11 每个概念对应一个词
给每个抽象概念选一个词,并且一以贯之。例如,使用fetch
、retrieve
和get
来给在多个类中的同种方法命名。你怎么记得住哪个类中是哪个方法呢?
同样,在同一堆代码中有controller
,又有manager
,还有driver
,就会令人困惑。DeviceManager
和ProtocolController
之间有何根本区别?为什么不全用 controllers
或 managers
?他们都是Drivers
吗?这种名称,让人觉得这两个对象是不同类型的,也分属不同的类。
2.12 别用双关语
避免将同一单词用于不同目的。同一术语用于不同概念,基本上就是双关语了。如果遵循“一词一义”规则,可能在好多个类里面都会有add方法。只要这些add方法的参数列表和返回值在语义上等价,就一切顺利。
但是,可能会有人决定为“保持一致”而使用add
这个词来命名,即便并非真的想表示这种意思。比如,在多个类中都有add
方法,该方法通过增加或连接两个现存值来获得新值。假设要写个新类,该类中有一个方法,把单个参数放到群集(collection
)中。该把这个方法叫做 add
吗?这样做貌似和其他 add
方法保持了一致,但实际上语义却不同,应该用 insert
或append
之类词来命名才对。把该方法命名为add
,就是双关语了。
代码作者应尽力写出易于理解的代码。我们想把代码写得让别人能一目尽览,而不必殚精竭虑地研究。我们想要那种大众化的作者尽责写清楚的平装书模式;我们不想要那种学者挖地三尺才能明白个中意义的学院派模式。
2.13 使用解决方案领域名称
只有程序员才会读你的代码。所以,尽管用那些计算机科学(Computer Science,CS)术语、算法名、模式名、数学术语吧。依据问题所涉领域来命名可不算是聪明的做法,因为不该让协作者老是跑去问客户每个名称的含义,其实他们早该通过另一名称了解这个概念了。
2.14 使用源自所涉问题领域的名称
如果不能用程序员熟悉的术语来给手头的工作命名,就采用从所涉问题领域而来的名称吧。至少,负责维护代码的程序员就能去请教领域专家了。
优秀的程序员和设计师,其工作之一就是分离解决方案领域和问题领域的概念。与所涉问题领域更为贴近的代码,应当采用源自问题领域的名称。
2.15 添加有意义的语境
提供语境,便于理解上下文代码。
很少有名称是能自我说明的——多数都不能。反之,你需要用有良好命名的类、函数或名称空间来放置名称,给读者提供语境。如果没这么做,给名称添加前缀就是最后一招了。
设想你有名为firstName
、lastName
、street
、houseNumber
、city
、state
和zipcode
的变量。当它们搁一块儿的时候,很明确是构成了一个地址。不过,假使只是在某个方法中看见孤零零一个state
变量呢?你会理所当然推断那是某个地址的一部分吗?
可以添加前缀addrFirstName
、addrLastName
、addrState
等,以此提供语境。至少,读者会明白这些变量是某个更大结构的一部分。当然,更好的方案是创建名为Address
的类。这样,即便是编译器也会知道这些变量隶属某个更大的概念了。
// 语境不明确变量
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);
}
// 语境明确变量
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";
}
}
2.16 不要添加没用的语境
设若有一个名为“加油站豪华版”(Gas Station Deluxe)的应用,在其中给每个类添加GSD前缀就不是什么好点子。说白了,你是在和自己在用的工具过不去。输入G,按下自动完成键,结果会得到系统中全部类的列表,列表恨不得有一英里那么长。这样做聪明吗?为什么要搞得IDE没法帮助你?
对于Address类的实体来说,accountAddress和customerAddress都是不错的名称,不过用在类名上就不太好了。Address是个好类名。如果需要与MAC地址、端口地址和Web地址相区别,我会考虑使用PostalAddress、MAC和URI。这样的名称更为精确,而精确正是命名的要点。