内容摘选自 《代码简洁之道》,作者:Robert C. Martin。仅供学习交流使用,后续会持续进行更新。
本次更新时间:2023年11月27日
下次更新:5天内
序言
本书提出了一种概念:代码质量与其整洁度成正比。
第 1 章 整洁代码
阅读本书有两种原因:第一,你是个程序员;第二,你想成为更好的程序员。
1. 什么是简洁代码
Bjarne Stroustrup, C++ 语言发明者:我喜欢优雅和高效的代码。代码逻辑应直截了当,叫缺陷难以隐藏;尽量减少依赖关系,使之便于维护;依据某种分层战略完善错误处理代码;性能调至最优,省得引诱别人做没规矩的优化,搞出一堆混乱来。整洁的代码只做好一件事。
Grady Booch, Object Oriented Analusis and design with applications 作者:整洁的代码简洁直接。整洁的代码如同优美的散文,充满干净利落的抽象和直接了当的控制语句。
光把代码写好可不够。必须时时保持代码整洁。我们都见过代码随着时间流逝而腐坏。我们应该更积极地阻止腐坏的发生。“让营地比你来时更干净”。
第 2 章 有意义的命名
我们给变量、函数、参数、源代码等等命名。我们命名、命名,不断命名。既然有这么多命名要做,不妨做好它。下文列出了取个好名字的几条规则。
2.1 名副其实
取名字这件事很 严肃。选个好名字要花时间,但是省下来的时间要比花掉的多。注意命名,而且一旦发现有更好的名称,就换掉旧的。
如果名称需要注释来补充,那就不算名副其实。
下面举几个例子:
int d; // 消逝的时间,以日计
名称 d 什么也没有说明,它没有引起对时间消逝的感觉,更别说以日计了。我们需要指明计量对象和计量单位的名称。选择体现本意的名称能让人更容易理解和修改代码。
int elapsedTimeInDays;
int daysSinceCreation;
int daysSinceModification;
int fileAgeInDays;
下面的代码意义是什么?
public List<int[]> getThem() {
List<int[]> list = new ArrayList<int[]>();
for (int[] x : theList)
if (x[0] == 4) {
list.add(x);
}
return list;
}
问题不在于代码的简洁度,而是在于代码的 模糊度:即上下文在代码中未被明确体现的程度。对于上述代码,我们有以下疑问:
- theList 中是什么类型的东西?
- theList 零下标条目的意义是什么?
- 值 4 的意义是什么?
- 我怎么使用返回的列表?
问题的答案没有体现在代码段中。
假如,我们在开发一种扫雷游戏,我们发现,盘面是名为 theList 的单元格列表,那就将其名称改为 gameBoard。
盘面上每个单元格都用一个简单数组表示。我们还可以发现,零下标条目是一种状态值,而这种状态值为 4 表示“已标记”。只要改为有意义的名称,代码就会得到相当程度的改进:
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 类型。
提防使用不同之处较小的名称。想区分模块中某处的 XYZControllerForEfficientHandlingOfStrings 和另一处的 XYZControllerForEfficientStorageOfStrings ,会花多次时间呢?这两个词的外形实在是太相似了。
2.3 做有意义的区分
以下面的代码为例。
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
一样,是意义含混的废话。
2.4 使用读的出来的名称
如果名称读不出来,讨论的时候就会像个傻鸟。
比如写了一个 genymdhms
(生成日期,年、月、日、时、分、秒),一般会读成(gen why emm dee aich emm ess)。这听起来傻乎乎的,不如直接改成 generationTimeStamp
。
现在读起来就像人话了:“喂,Mikey,看看这条时间戳!”
2.5 使用可搜索的名称
单字母名称和数字常量有个问题,就是很难在一大篇文字中找出来。
在 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_PF_TASKS; j++) {
int realTaskDays = taskEstimate[j] * realDaysPerIdealDay;
int realTaskWeeks = (realDays / WORK_DAYS_PER_WEEK);
sum += realTaskWeeks;
}
注意,上面代码中的 sum
并非特别有用的名称,不过它至少搜得到。采用能表达意图的名称,貌似拉长了函数代码,但要想想看,WORK_DAYS_PER_WEEK
比 5
要好找的多。
2.6 避免使用编码
把 类型 或者 作用域 写进名称中,陡然增加了解码的负担。没理由要求每位新人都在弄清要应付的代码之外,还要再搞懂另一种编码语言。这对于解决问题而言,纯属多余的负担。带编码的名称也不便发音,容易打错。
2.6.1 匈牙利语标记法
据说这种命名法是一位叫 Charles Simonyi 的匈牙利程序员发明的,后来他在微软待了几年,于是这种命名法就通过微软的各种产品和文档资料向世界传播开了。匈牙利语法的格式一般为:变量名=属性+类型+对象描述。
例如:
hwnd
:h
是类型描述,表示句柄,wnd
是对象描述,表示窗口,所以hwnd
表示窗口句柄。g_cch
:g_
是属性描述,表示全局变量,c
和ch
分别是计数类型和字符类型,一起表示变量类型,这里忽略了对象描述,所以它表示一个对字符进行计数的全局变量。
早期的编译器不会对变量做类型检查,程序员需要匈牙利语标记法来帮助自己记住类型。
现代编程语言有更丰富的类型系统,编译器也记得变量的类型,代码环境已经先进到再变异开始之前就侦测到类型错误的程度!所以,这种类型编码纯属多余。他们增加了修改变量、函数或类的名称或类型的难度。他们增加了代码阅读的难度。
这里补充现在几个常见的编码类型:
驼峰式命名法:第一个单词首字母小写,后面其他单词首字母大写。java
中的变量命名就经常使用驼峰命名法。
char[] myName;
int myAge;
帕斯卡命名法:又叫大驼峰命名法,每一个单词的首字母都大写。c#
中使用的比较多。
char[] MyName;
int MyAge;
下划线命名法:单词与单词之间用下划线连接,所有字母都小写。python
中使用的较多。
my_name = 'zs';
my_age = 1;
2.6.2 成员前缀
不必使用 m_
前缀来标明成员变量。应该把类和函数做的足够小,消除对成员前缀的需要。你应该使用某种可以高亮或者用颜色标出成员变量的编码环境。
public class Part {
private String m_dsc; // 不要使用这种方式
private String description; // 推荐方式
}
此外,人们会很快学会无视前缀(或后缀),只看到名称中有意义的部分。代码读的越多,眼中就越没有前缀。最终,前缀变成了不入法眼的废料,变成了旧代码的标志物。
2.6.3 接口和实现
有时候也会出现采用编码的特殊情形。比如,你在做一个创建形状用的抽象工厂(Abstract Factory)。该工厂是一个接口,要用具体类来实现。你怎么来命名工厂和具体类呢?IShapeFactory
和 ShapeFactory
吗?
我更倾向于不加修饰的接口。前导字母 I
被滥用到了说好听点是干扰,说难听点就是废话。如果接口和实现必须要选择一个来编码的话,我宁肯选择实现。ShapeFactoryImp
比对接口编码要好的多。
2.7 避免思维映射
不应该让读者在脑中把你的名称翻译为他们熟知的名称。
单字母的变量名就是个问题。在作用域较小,也没有名称冲突时,循环计数器自然有可能被命名为 i
或 j
或 k
。这个时候对于其他变量的命名就不宜再使用 l
这样的单字母了,因为会被认为是某个计数器。
专业程序员能善用其能,编写其他人能理解的代码。
2.8 类名
类名和对象名应该是 名词 或者 名词短语,如 Customer
、WikiPage
、Account
和 AddressParser
。避免使用Manager
、Processor
、Data
或 Info
这样的类名,因为他们指代不清楚,可以修改为名词短语。如 HotelManager
。类名不应当是动词。
2.9 方法名
方法名应该是 动词 或者 动词短语,如 postPayment
、deletePage
或 save
。
String name = employee.getName();
employee.setName('mike');
2.10 别扮可爱
如果名称太耍宝,那就只有同作者一样有幽默感的人才能记住,而且还是在他们记得那个笑话的时候才行。谁会之道 HolyHandGrenade
的函数是干什么的呢?没错,这个名字挺伶俐的,不过 DeleteItems
或许是更好的名称。宁可明确,毋为好玩。( HolyHandGrenade
,意味圣手手雷,破坏除防爆物体以外的物品。DeleteItems
,删除条目。)
扮可爱的做法在代码中常体现为使用俗话或者俚语。例如,别用 whack()
表示 kill()
。别用 eatMyShorts()
这类与文化相关的笑话来表示 abort()
。(whack
,劈砍。eatMyShorts
,去死吧)
2.11 每个概念对应一个词语
给每个抽象概念选一个词,并一以贯之。
例如,使用 fetch
、retrive
和 get
来给在多个类中的同样方法命名。你怎么记得哪个类中是哪个方法呢?
同样,在同一堆代码中有 controller
,又有 manager
就会令人困惑。DeviceManager
和 DeviceController
之间有什么区别呢?这让人觉得这两个对象是不同类型的,也分属不同的类。
对于那些会使用到你代码的程序员,一以贯之的命名法简直就是泼天富贵。
2.12 别用双关语
避免将同一单词用于不同目的。同一术语用于不同的概念,基本上就是双关语了。
比如,我们有一个 add()
方法,该方法将通过增加或者连接两个现存的值来获得新值。假如现在又有一个add()
方法,需要实现将一个值添加到一个群组中(方法三)。
# 方法一,将某个值 +1
value = 0
def add():
global value # python代码,代表使用外部的变量
value += 1
# 方法二,返回两数之和
def add(val1, val2):
return val1 + val2
# 方法三,添加某个值到群组中
def add(my_list, value):
my_list.append(value)
方法三貌似和其它方法保持了一致,但实际语义不同,应该用 insert
或者 append
之类的词来命名。把该方法命名为 add
,就是双关语了。
2.13 使用领域解决方案名称
记住,只有程序员才会读你的代码。所以,尽管使用那些计算机科学属于、算法名、模式名、数学术语吧。
对于熟悉访问者 ( VISITOR
) 模式的程序员来说,名称 AccountVisitor
富有意义。哪个程序员会不知道 JobQueue
的意思呢? 程序员要做太多技术性工作。给这些事取个技术性的名称,通常是最靠谱的做法。
如果不能使用程序员熟悉的术语来给手头的工作命名,就采用从所涉问题领域而来的名称吧。至少,负责维护代码的程序员就能请教领域专家了。
2.14 添加有意义的语境
很少有名称是能自我说明的 ------ 多数都不能。反之,你需要有良好命名的类、函数或者名称空间来放置名称,给读者提供语境。如果你没有这么做,给名称添加前缀就是最后一招。
设想你有名为 firstName
、lastName
、street
、houseNumber
、city
、state
和 zipcode
的变量。当它们搁一块的时候,很明确是构成了一个地址。不过,假使只看到一个 state
变量呢?你会理所当然推断那是某个地址的一部分吗?
你可以添加前缀 addrFirstName
、addrLastName
、addrState
等,以此提供语境。至少读者会明白这些变量是某个更大结构的部分。当然了,更好的方案是创建名为 Address
的类。这样,即便是编译器也会知道这些变量隶属于某个更大的概念了。
看看下面的代码。以下变量是否需要更有意义的语境呢?函数名仅给出了部分语境;算法提供剩下的部分。浏览函数后,你才会知道 number
、verb
和 pluralModifier
这三个是猜测的信息的一部分。不幸的是这语境得靠读者推断出来。第一眼看到这个方法时,这些变量的含义完全不清楚。
// 语境不明确的变量
private void printGuessStatistice(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);
}
// 部分输出值
// There are 5 ps
// There is 1 m
上述的函数有点儿长,变量的使用贯穿始终。要分解这个函数,需要创建一个名为 GuessStatisticsMessage
的类,把三个变量做成该类的成员字段。这样它们就在定义上变成了 GuessStatisticsMessage
的一部分。语境的增强也让这个算法能够通过编写更小的函数而变得干净利落。
// 有语境的变量
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 thereAreNoLetters(){
number = "no";
verb = "are";
pluralModifier = "s";
}
private void thereIsOneLetter(){
number = "1";
verb = "is";
pluralModifier = "";
}
private void thereAreManyLetters(int count){
number = Integer.toString(count);
verb = "are";
pluralModifier = "s";
}
}