为什么要讲代码可读性-编辑器回放
在讲代码可读性之前,我们想想为什么要讲代码可读性?代码可读性究竟有这么重要吗?先来看一个下例子。
你是否也玩过编辑器回放?有的编辑器可以记录每次击键动作,当一段时间后回放击键过程,就像是看一部告诉电影,十分有趣。回放的过程显示,多数时间我们是在滚动屏幕、阅读代码,就像这样。
- Bob进入模块
- 他向下滚动到要修改的函数
- 他停下来考虑可以做什么
- 他滚动到模块顶端,检查变量初始化
- 现在他回到修改处,开始键入
- 他删掉了键入的内容
- 他又重新键入
- 他又删除了
- 他键入了一半什么东西,又删掉了
- 他滚动到调用要修改函数的另一函数,看看是怎么调用的
- 他回到修改处,重新键入刚才删掉的代码……
可能你已经发现,我们在写代码的时候花了大量的时间阅读老代码。写新代码的时间相对于阅读老代码的时间非常少。那么,我们是不是该谈一谈代码的可读性呢?
本文将从两方面来讲代码可读性,命名和函数。
命名
- 名副其实
- 避免误导
- 避免空泛的名字
- 避免单字母命名
- 有意义的区分
- 为名称附带跟多的信息
- 使用读得出来的名字
- 可搜索的名字
- 类名
- 方法名
- 每个概念对应一个词
- 使用专业的单词
- 为作用域大的名字采用更长的名字
- 只使用众所周知的缩写
- 尊重开发人员的直觉
- Airbnb命名规则
1.名副其实
一段名不副实的代码是这样的。也许读者能看懂这段代码要做件什么事儿,但是无法理解作者到底要做一件怎样的事儿。
public List<int []> getThem() {
List <int[]> list1 = new ArrayList<int[]>();
for (<int[]> x : theList)
if (x[0] == 4)
list1.add(x);
return list1;
}
但是稍加修改一下,这段代码可读性大大提高。我们能重头到尾很顺畅的读懂这段代码,就是要把被标记了的格子放到一个叫flaggedCells
的数组里面。就像读一个小故事一样简单。名副其实的命名一定要表达该函数或常量的真实意义
public List<int []> getFlaggedCells() {
List <int[]> flaggedCells = new ArrayList<int[]>();
for (<int[]> cell : gameBoard)
if (cell[STATUS_VALUE] == FLAGGED)
flaggedCells.add(cell);
return flaggedCells;
}
2.避免误导
在DataSetDataPreview这个组件里,有dataSetPreviewData和previewData两个误导读者的属性,这两个属性看似一样,无法分辨其不同之处,严重误导了读者。如果修改代码时,读到了这里。读者需要花大量的时间去分辨这俩个属性的具体含义,严重的降低工作效率。
<DataSetDataPreview
dataSetPreviewData={this.props.dataSetPreviewData}
fields={fieldObj}
previewData={this.props.previewData}
onUpdate={this.props.onUpdatePreview}
/>
3.避免空泛的名字
避免使用像tmp、retval这样的名字。空泛的名字无法表达应有的含义,尽量用具有描述性的名字。
String tmp = user.name();
tmp += " " + user.phone_number();
tmp += " " + user.email();
...
template.set("user_info", tmp);
4.避免单字母命名
避免像f
这样子的命名,这里的f
完全不能表达该变量的实际意义。单字母命名实在是很让人费解。
<tr>{this.headerItems(f => f.displayName)}</tr>
尽管在map中,同样推荐将变量命名完整一点。比如下面⬇️的column。
return map(previewData.columns, column => {
const field = fieldMap[column]
…
})
但是如果在单行的函数中,可以酌情使用。例如,由于有了前面的columns,我们很容易联想到c
指代的时column
。
map(columns, (c) => c.xxx )
5.有意义的区分
避免使用ProductInfo、ProductData这样的名称,这两个名字看起来和Product没有什么区别。同样不要使用类似theUser、nameString的名称,theUser和user没有什么区别,同时name也不可能是个数字或者布尔值吧。
- varibale一词永远不应当出现在变量名中。
- 要区分名称,就要以读者能鉴别不同之处的方式来区分。
6.为名称附带更多的信息
尽可能在名字中把名称应该表达的信息都表达出来。
- 如果是命名一个ID的话,使用currentPageId肯定比CurrentPage好得多。
- 在一个满是dataSet的组件中,selectedDataSet比dataset更能表明这是一个选中的dataSet。
7.使用读的出来的名字
不知道你有没有把文字在心中或者嘴里念出来的习惯,据我所知很多人是会在脑子里读一下。genymdhms这样子的名称先不讨论其表达的含义,如果碰见这样子的名称,该怎么读呢?
…
private Date genymdhms;
private Date modmdhms;
…
如果这样子命名是不是好读得多?
…
private Date generationTimestamp;
private Date modificationTimestamp;
…
8、可搜索的名字
为什么要使用可以搜索的名字?想找到WOERK_DAY_PRE_WEEK很容易,但是想找到数字5就不那么容易了。
- 作用域越大越需要搜索
- 少使用缩写
- 名称做够详尽(变量名要详细)
const WOERK_DAY_PRE_WEEK =5
如果你是这样子命名的,那想要搜索找到User可能没那么容易吧。
models/User.js
controllers/User.js
services/User.js
presentations/User.js
…
xxx/User.js
…
把名字写详尽,比如像这样子。搜索User或者UserController变得轻而易举。
models/User.js
controllers/UserController.js
services/UserService.js
presentations/UserPresentation.js
9.类名
类名和对象名应该是名词或名词短语,如Customer、WikiPage、Account
和AddressParser。避免使用Manager、Processor、Data或Info这样的类名。类名不应当是动词。
10.方法名
方法名应当是动词或动词短语,如postPayment、deletePage或save。
11.每个概念对应一个词
例如在新建数据集里,有这些组件,但实际上都是一个概念,表示新建。应当用同一个名字来表述,create或者add取其一。
<AddDataSetRDB />
<AddDataSetReplacement />
<AddDataSetStep />
<CreateDataSetHome />
<CreateDataSetItem />
12.使用专业的单词
同样是从一个url获取page,可能是从本地、网上获取,或者是从网上下载。看起来都可以用getPage来表示,但是请使用专业的单词来区分一下。例如:
- 如果从本地获取,且轻量级的
getPage(url)
- 如果从网上获取
fetchPage(url)
- 如果从网上下载
downloadPage(url)
下面这个类,作者期望Size()方法返回什么呢?树的高度、节点数还是数在内存中所占的空间?size()可以指代很多东西,size()的范围太广了。
class BinaryTree {
int size();
...
};
Size()没有承载很多信息,更专业的词应该是height()、nodesCount()。
class BinaryTree {
int height();
int nodesCount();
...
};
13.为作用域大的名字采用更长的名字
在单个循环中使用i、j、k这样子的单字母命名并无大碍。但是像这样在多重嵌套循环的情况,再使用i、j、k,读起来就会感到吃力了。
for (int i = 0; i < clubs.size(); i++)
for (int j = 0; j < clubs[i].members.size(); j++)
for (int k = 0; k < users.size(); k++)
if (clubs[i].members[k] == users[j])
cout << "user[" << j << "] is in club[" << i << "]" << endl;
给i、j、k赋予更有意义的名字,例如clubIndex、memberIndex、userIndex。这样子就一目了然了。
for (int clubIndex = 0; clubIndex < clubs.size(); clubIndex++)
for (int memberIndex = 0; memberIndex < clubs[clubIndex].members.size(); memberIndex++)
for (int userIndex = 0; userIndex < users.size(); userIndex++)
if (clubs[clubIndex].members[memberIndex] == users[j])
cout << "user[" << memberIndex << "] is in club[" << clubIndex << "]" << endl;
14.只使用众所周知的缩写
缩写一定只使用众所周知的,千万不要使用自己编造的缩写。不然就只有作者和上帝知道是什么意思了,可能过一段时间后就只有上帝知道什么意思了。看下面第三行中fetchNParseHtml这个名字,你知道中间大写的N是什么意思吗?其实N是and的意思,这并不是自己编造缩写,但是并不属于众所周知的缩写。还是尽量不要使用的好。而且在满是驼峰命名的代码里,一个大写的N并不是那么明显。
document --> doc
string --> str
fetchNParseHtml --> ?
15.尊重开发人员的直觉
取一个合适的名字,让其他开发者能明白calculateMean()是一个会消耗一定资源的方法,不然其他开发者可能会随意使用这个方法,而不考虑性能问题。
public class StatisticsCollector {
public void addSample(double x) { ... }
public double getMean() {
// Iterate through all samples and return total / num_samples }
...}
public class StatisticsCollector {
public void addSample(double x) { ... }
public double calculateMean() {
// Iterate through all samples and return total / num_samples }
...}
16.Airbnb命名规则
对比一下Airbnb的JavaScript命名规范,基本与上述原则一致。
- 避免单字母命名。命名应具备描述性
// bad
function q() {
// ...stuff...
}
// good
function query() {
// ..stuff..
}
- 使用驼峰式命名对象、函数和实例
// bad
const OBJEcttsssss = {};
const this_is_my_object = {};
function c() {}
// good
const thisIsMyObject = {};
function thisIsMyFunction() {}
- 使用帕斯卡式命名构造函数或类
// bad
function user(options) {
this.name = options.name;
}
// good
class User {
constructor(options) {
this.name = options.name;
}
}
- 使用下划线 _ 开头命名私有属性
// bad
this.__firstName__ = 'Panda';
this.firstName_ = 'Panda';
// good
this._firstName = 'Panda';
- 如果你的文件只输出一个类,那你的文件名必须和类名完全保持一致
// file contents
class CheckBox {
// ...
}
export default CheckBox;
// in some other file
// bad
import CheckBox from './checkBox';
// bad
import CheckBox from './check_box';
// good
import CheckBox from './CheckBox';
- 当你导出默认的函数时使用驼峰式命名。你的文件名必须和函数名完全保持一致
function makeStyleGuide() {
}
export default makeStyleGuide;
- 当你导出单例、函数库、空对象时使用帕斯卡式命名
const AirbnbStyleGuide = {
es6: {
}
};
export default AirbnbStyleGuide;
函数
函数与可读性有什么关系呢?先看一个例子:
handleAppend() {
const { form } = this.props
const charset = this.props.charset
const options = form.Append.options
if (this.state.headerValid == false) {
Notification.warn('请选择对应的文件')
return
}
const path = this.props.form.Append.options.textPath.value
if (isUndefined(options.description) || isEmpty(options.description.value)) {
Notification.warn('请填写数据说明')
return
}
const description = this.props.form.Append.options.description.value
const dataSetId = this.props.relatedDataSet.id
let data = {}
if (this.props.relatedDataSource.dataSourceType == 'CSV') {
data.csvPath = path
data.charset = charset
} else {
data.excelPath = path
}
data.description = description
this.props.dataSetAppend(dataSetId, data).then((action) => {
if (action.errors && !action.message) {
if (action.errors[0]) {
Notification.warn(action.errors[0], 1)
} else {
Notification.warn(action.message, 1)
}
} else {
Notification.success('追加成功')
this.props.updateSelectedTabIndex(5)
this.props.loadImportHistory(this.props.relatedDataSet.id)
this.props.goToDataSets()
this.handleCleanAppendFields()
this.handleCleanAppendPreview()
this.props.cleanCharSet()
}
})
}
这个函数有41行之多,可以看到函数里面做了三件事。验证、组装请求数据和调用action。当我看到要改这个函数时就感到非常可怕,内心是拒绝去看这个函数。因为它实在太长了。后来把它改成这样,把验证、组装请求数据和请求成后的回调分别抽离出来,似乎可读性变高了。
handleConfirmAppend() {
const {
appendLocalFileRelatedDataSet,
dataSetAppend,
isHeaderValid,
appendForm: { options: { description } }
} = this.props
const validate = validateDescriptionAndHeader(description, isHeaderValid)
if (validate.hasError) return message.error(validate.message)
const dataSetId = appendLocalFileRelatedDataSet.id
const requestBody = this.composeAppendRequestBody()
dataSetAppend(dataSetId, requestBody).then((response) => {
responseNotification(response, '追加成功', this.handleAppendSuccessCallBack)
})
}
提高函数的可读性,可以从以下几个方面着手:
- 短小
- 只做一件事
- 函数参数
- 无副作用
- 別重复自己
- 如何写出这样的函数
1.短小
函数的第一规则是要短小,第二条规则则是还要更短小。
函数越短小,读者越容易看懂这个函数。短小的函数比起大函数更有条理,让读者阅读起来更轻松。
2.只做一件事
函数应该做一件事,做好这件事,只做一件事。
函数应该只做与之名字相对应的事,否则就有可能使读者产生误解。如果读者不熟悉函数内部做了其他事,就会增加改出Bug得几率。因为他并不知道你还做了其他的事儿。
3.函数参数
最理想的参数数量是零,其次是一,再次是二,应尽量避免三
4.无副作用
函数承诺只做一件事,但还是会做其他被藏起来的事
在这个UserValidator的类中,不光对user做了验证,同时还做了初始化,这就是一个副作用
public class UserValidator (
…
if ("Valid Password".equals(phrase)) {
Session.initialize();
…
}
…
)
如果一定要时序性耦合,就应当在函数名称中说明,例如把UserValidator改成checkPasswordAndInitializeSession
5.別重复自己
重复可能是软件中一切邪恶的根源
6.如何写出这样的函数
在写代码时,实在是很难做到以上这几点。但这并不能成为我们写烂代码的借口。但是我们可以这样子做,来保证我们的代码质量。
- 想什么写什么
- 然后打磨它
- 单元测试
- 分解函数
- 修改名称
- 消除重复
- 保持测试通过
在不久的将来,我们写的每一段代码,都有可能被自己或者别的开发者阅读修改。如果想要给未来的自己或者其他开发者省一点时间,建议在创造代码时一定要考虑到将来读者的感受,一定要让读者看到你的代码时不得不说出赞美的词。