代码整洁之道之变量

整洁代码

C++语言发明者

我喜欢优雅和高效的代码。代码逻辑应该直接了当,叫缺陷难以隐藏;尽量减少依赖关系。使之便于维护;依据某种分层战略完善错误代码;性能调至最优,省的引诱别人做没规矩的优化,搞出一堆混乱来。整洁的代码只做好一件事。

Dave Thomas OTI 公司创始人,Eclipse 战略之父

整浩的代码应可由作者之外的开发者阅读和增补。它应有单元测试和验收测试。它使用有意义的命名。它只提供而非多种做一件事的途径。它只有尽量少的依赖关系,要明确地定义和提供清晰、尽量少的API. 代码应通过其葡表达含义,因为不同的语言导致并非所有必需信息均可代码自身清晰表达。

什么属于整洁代码? wiki 发明者

  • 能通过所有测试
  • 没有重复代码
  • 体现系统中的全部理念
  • 尽量包含少的实体, 比如:类、方法和函数

变量的命名

有意义的命名

命名随处可见,变量、函数、参数和包命名,好的命名不仅提高代码的阅读性更能提高可维护性。

好的命名方式的几条简单规则:

名副其实

什么是名副其实?就是变量、函数或类的名称应该已经答复了所有的大问题。它该告诉你,它为什么会存在,它做什么事,应该怎么用。如果名称需要注释来补充,那就不算是名副其实。

如:

1
int d; // 消逝的时间,以日计

名称d什么也没说明。它没有引起对时间消逝的感觉,更别说以日计了。

好的命名方式,明确指定了计量对象和计量单位的名称,如:

1
2
3
4
int elapsedTimeInDays;
int daysSinceCreation;
int daysSinceModification;
int fileAgeInDays;

选择体现本意的名称能让人更容易理解和修改代码。

下列代码的目的何在?

1
2
3
4
5
6
7
public List<int[]> getThem () {
List<int[]> listl = new ArrayList<int[]>() ;
for (int [] x : theList)
if (x[0] == 4)
listl.add (x);
return list1;
}

为什么难以说明上列代码要做什么事?里面并没有复杂的表达式。空格和缩进中规中矩。只用到三个变量和两个常量。甚至没有涉及任何其他类或多态方法,只是(或者看起来是)
一个数组的列表而已。

问题不在于代码的简洁度,而是在于代码的模糊度:即上下文在代码中未被明确体现的
程度。上列代码要求我们了解类似以下问题的答案:

  1. theList 中是什么类型的东西?
  2. theList 零下标条目的意义是什么?
  3. 值4的意义是什么?
  4. 我怎么使用返回的列表?

加入在开发一种扫雷游戏,我们发现,盘面是名为theList 的单元格列表,那就将其名称改为 gameBoard。盘面上每个单元格都用一个简单数组表示。我们还发现,零下标条目是一种状态值,而该种状态值为4表示“已标记”。只要改为有意义的名称,代码就会得到相当程度的改进:

1
2
3
4
5
6
7
public List<int[]> getFlaggedCells ()
List<int[]> flaggedCells = new ArrayList<int[]>();
for (int[] cell : gameBoard)
if (cell[STATUS_VALUE]
flaggedCells.add (cell);
return flaggedCells;
}

注意,代码的简洁性并未被触及。运算符和常量的数量全然保持不变,嵌套数量也全然保持不变。但代码变得明确多了。

还可以更进一步,不用 int 数组表示单元格,而是另写一个类。该类包括一个名副其实的函数(称为isFlagged),从而掩盖住那个魔术数。于是得到函数的新版本:

1
2
3
4
5
6
7
public List<Cell> getFlaggedCells ()
List<Cell> flaggedCells = new ArrayList<Cell>();
for (Cell cell : gameBoard)
if (cell.isFlagged ())
flaggedCells.add (cell);
return flaggedCells;
}

只要简单改一下名称,就能轻易知道发生了什么。这就是选用好名称的力量。

避免误导

程序员必须避免留下掩藏代码本意的错误线索。应当避免使用与本意相悖的词。例如,
hp、aix 和 sco 都不该用做变量名,因为它们都是UNIX 平台或类 UNX 平台的专有名称。

别用 accountList 来指称一组账号,除非它真的是List类型。List 一词对程序员有特殊意
义。如果包纳账号的容器并非真是个 List,就会引起错误的判断”。所以,用 accountGroup 或
bunchOfAccounts,甚至直接用 accounts 都会好一些。

提防使用不同之处较小的名称。想区分模块中某处的 XYZControllerForEfficientHandlingOfStrings 和另一处的XYZControllerForEfficientStorageOfStrings, 会花多长间呢?这两个词外形实在太相似了。

键入某个名称的前几个字母,按一下某个热键组合(如果有的话),就能得到一列该名称的可能形式。假如相似的名称依字母顺序放在一起,且差异很明显,那就会相当有助益,因为程序员多半会压根不看你的详细注释,甚至不看该类的方法列表就直接看名字挑一个对象。

误导性名称真正可怕的例子,是用小写字母1和大写字母O作为变量名,尤其是在组合使
用的时候。当然,问题在于它们看起来完全像是常量“壹”和“零”。

1
2
3
4
5
int a = 1;
if (O == 1)
a = O1;
else
l = 0l;

做有意义的区分

如:

1
2
3
4
5
public static void copyChars (char al[0, char a2[]) {
for (int i = 0; i < al.length; i++) {
a2 [i] = al [i];
}
}

如果参数名改为 source 和 destination,这个函数就会像样许多。
废话是另一种没意义的区分。

假设你有一个 Product 类。如果还有一个 ProductInfo 或 ProductData 类,那么他们的名称虽然不同,意思却无 区别。Info 和 Data 就像a、an、the一样是意义含混的话。

注意,只要体现出有意义的区分,使用 a 和 the 这样的前缀就没错。

废话都是冗余。Variable 一词永远不应当出现在变量名中。Table 一词永远不应当出现在表名中。NameString 会比 Name 好吗?难道 Name 会是一个浮点数不成?如果是这样,就触犯了关于误导的规则。设想有个名为 Customer 的类,还有一个名为 CustomerObject 的类。区别何在呢?哪一个是表示客户历史支付情况的最佳途径?

有个应用反映了这种状况。为当事者讳,我们改了一下,不过犯错的代码的确就是这个样子:

1
2
3
getActiveAccount ();
getActiveAccounts ();
getActiveAccountInfo ();

程序员怎么能知道该调用哪个函数呢?

如果缺少明确约定,变量 moneyAmount 就与 money 没区别,customerInfo 与customer没区别, accountData 与 account 没区别,theMessage 也与 message 没区别。要区分名称,就要以读者能鉴别不同之处的方式来区分

使用读的出来的名字

人类长于记忆和使用单词,使用读的出来的名字更能加深我们的记忆。

1
2
3
4
5
6
7
class DtaRcrd102 {
private Date genymdhms;
private Date modymdhms;
private final String pszqint = "102"
/* ... */
};

1
2
3
4
5
6
class Customer {
private Date generationTimestamp;
private Date modificationTimestamp;;
private final String recordId = "102";
/* ... */
}

使用可搜索的名称

避免单字母,单字母名称和数字常量有个问题,就是很难在一大篇文字中找出来。可搜索的名字更有助于IDE的搜索和替换。

找MAX_CLASSES_PER_STUDENT 很容易,但想找数字 7 就麻烦了,它可能是某些文件名或其他常量定义的一部分,出现在因不同意图而采用的各种表达式中。如果该常量是个长数字,又被人错改过,就会逃过搜索,从而造成错误。

同样,e也不是个便于搜索的好变量名。它是英文中最常用的字母,在每个程序、每段代码
中都有可能出现。由此而见,长名称胜于短名称,搜得到的名称胜于用自造编码代写就的名称。

单字母名称仅用于短方法中的本地变量。名称长短应与其作用域大小相对应。若变量或常量可能在代码中多处使用,则应赋其以便于搜索的名称。再比较

1
2
3
for (int j=0; j<34; j++) {
s += (t[j]*4)/5;
}

1
2
3
4
5
6
7
8
int realDays PerIdealDay
const int WORK_DAYS_PER_WEEK = 5;
int sum = 0;
for (int j=0; j < NUMBER_OF_TASKS; j++) {
int realTaskDays = taskEstimatel[j] * realDaysPerIdealDay;
int realTaskWeeks = (realdays / WORK_DAYS_PER_WEEK);
sum += realTaskWeeks;
}

避免前缀

应该把类和函数做的足够小,不必用类似m_来标明前缀。

1
2
3
4
5
6
7
public class Part {
private String m_dsc; // The textual description
void setName (String name) {
m_dsc = name;
}

}

1
2
3
4
5
6
public class Part {
String description;
void setDescription (String description) {
this.description = description;
}
}

此外,人们会很快学会无视前缀(或后缀),只看到名称中有意义的部分。代码读得越多,眼中就越没有前缀。最终,前缀变作了不入眼的废料,变成了旧代码的标志物。

接口和实现

比如,在做一个创建形状用的抽象工厂(Abstract Factory)。该工厂是个接口,要用具体类来实现。你怎么来命名工厂和具体类呢? IShapeFactory和ShapeFactory 吗?前导字母I被滥用到了说好听点是干扰,说难听点根本就是废话的程度。如果接口和实现必须选一个来编码的话,ShapeFactoryImp,甚至是丑陋的CShapeFactory,都比对接口名称编码来的好。

避免映射思维

不应当让读者在脑中把你的名称翻译为他们熟知的名称。这种问题经常出现在选择是使用问题领域术语还是解决方案领域术语时。

单字母变量名就是个问题。在作用域较小、也没有名称冲突时,循环计数器自然有可能被命名为i或j或 k。(但千万别用字母l!) 这是因为传统上惯用单字母名称做循环计数器。然而,在多数其他情况下,单字母名称不是个好选择;读者必须在脑中将它映射为真实概念。

类名

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

方法名

方法名应当是动词或动词短语,如 postPayment、deletePage 或 save。属性访问器、修改器和断言应该根据其值命名,并依 Javabean 标准”加上 get、set 和 is 前缀。

1
2
3
string name = employee.getName();
customer.setName ("mike");
if (paycheck.isPosted (0)...

使用常用单词

不要使用不常用的或者只有自己知道的单词。

例如,别用 whack()来表示kill()。别用 eatMyShorts()?这类与文化紧密相关的笑话来表示 abort()。言到意到。意到言到。

每个概念对应一个词

给每个抽象概念选一个词,并且一以贯之。例如,使用 fetch、retrieve 和get类中的同种方法命名。你怎么记得住哪个类中是哪个方法呢?

别用双关语

避免将同一单词用于不同目的。同一术语用于不同概念,基本上就是双关语了。如果遵循“一词一义”规则,可能在好多个类里面都会有 add 方法。只要这些 add 方法的参数列表和返回值在语义上等价,就一切顺利。

但是,也不要为“保持一致”而使用 add 这个词来命名,即便并非真的想表示这种意思。比如,在多个类中都有add 方法,该方法通过增加或连接两个现存值来获得新值。假设要写个新类,该类中有一个方法,把单个参数放到群集(collection)中。该把这个方法
叫做 add 吗?这样做貌似和其他 add 方法保持了一致,但实际上语义却不同,应该用 insert 或 append 之类词来命名才对,把该方法命名为 add,就是双关语了。

使用解决方案领域名称

只有程序员才会读你的代码码。所以,尽管用那些计算机科学(Computer Science, CS)术语、算法名、模式名、数学术语命名。

对于熟悉访问者(VISITOR)模式的程序来说,名称 AccountVisitor 富有意义。哪个程
序员会不知道 JobQueue 的意思呢?

使用源自所涉问题领域的名称

如果不能用程序员熟悉的术语来给手头的工作命名,就采用从所涉问题领域而来的名称吧。至少,负责维护代码的程序员就能去请教领域专家了。

添加有意义的语境

需要用有良好命名的类、函数或名称空间来放置名称,给读者提供语境。如果没这么做,给名称添加前缀就是最后一招了。

设想你有名为firstName、lastName、street、houseNumber、city、state 和 zipcode 的变量。它们搁一块儿的时候,很明确是构成了一个地址。不过,假使只是在某个方法中看见孤零零一个 state 变量呢?你会理所当然推断那是某个地址的一部分吗?

可以添加前缀 addrFirstName、addrLastName、addrState 等,以此提供语境。至少,读者会明白这些变量是某个更大结构的一部分。当然,更好的方案是创建名为 Address 的类。这
样,即便是编译器也会知道这些变量隶属某个更大的概念了。

不要添加没用的语境

只要短名称足够清楚,就要比长名称好。别给名称添加不必要的语境。对于 Address 类的实体来说,accountAddress 和 customerAddress 都是不错的名称,不过用在类名上就不太好了。Address 是个好类名。如果需要与MAC地址、端口地址和 Web 地址相区别,我会考虑使用 PostalAddress、MAC 和 URI。这样的名称更为精确,而精确正是命名的要点。

总结:

取好名字最难的地方在于需要良好的描述技巧和共有文化背景。好的名字能让代码阅读起来更为直接,更好维护。

参考

  • 《代码整洁之道》
Search by:GoogleBingBaidu