代码可读性-命名和函数

原文地址

为什么要讲代码可读性-编辑器回放

在讲代码可读性之前,我们想想为什么要讲代码可读性?代码可读性究竟有这么重要吗?先来看一个下例子。

你是否也玩过编辑器回放?有的编辑器可以记录每次击键动作,当一段时间后回放击键过程,就像是看一部告诉电影,十分有趣。回放的过程显示,多数时间我们是在滚动屏幕、阅读代码,就像这样。

  • Bob进入模块
  • 他向下滚动到要修改的函数
  • 他停下来考虑可以做什么
  • 他滚动到模块顶端,检查变量初始化
  • 现在他回到修改处,开始键入
  • 他删掉了键入的内容
  • 他又重新键入
  • 他又删除了
  • 他键入了一半什么东西,又删掉了
  • 他滚动到调用要修改函数的另一函数,看看是怎么调用的
  • 他回到修改处,重新键入刚才删掉的代码……

可能你已经发现,我们在写代码的时候花了大量的时间阅读老代码。写新代码的时间相对于阅读老代码的时间非常少。那么,我们是不是该谈一谈代码的可读性呢?

本文将从两方面来讲代码可读性,命名和函数。

命名

  1. 名副其实
  2. 避免误导
  3. 避免空泛的名字
  4. 避免单字母命名
  5. 有意义的区分
  6. 为名称附带跟多的信息
  7. 使用读得出来的名字
  8. 可搜索的名字
  9. 类名
  10. 方法名
  11. 每个概念对应一个词
  12. 使用专业的单词
  13. 为作用域大的名字采用更长的名字
  14. 只使用众所周知的缩写
  15. 尊重开发人员的直觉
  16. 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. 只做一件事
  3. 函数参数
  4. 无副作用
  5. 別重复自己
  6. 如何写出这样的函数

1.短小

函数的第一规则是要短小,第二条规则则是还要更短小。

函数越短小,读者越容易看懂这个函数。短小的函数比起大函数更有条理,让读者阅读起来更轻松。

2.只做一件事

函数应该做一件事,做好这件事,只做一件事。

函数应该只做与之名字相对应的事,否则就有可能使读者产生误解。如果读者不熟悉函数内部做了其他事,就会增加改出Bug得几率。因为他并不知道你还做了其他的事儿。

3.函数参数

最理想的参数数量是零,其次是一,再次是二,应尽量避免三

4.无副作用

函数承诺只做一件事,但还是会做其他被藏起来的事

在这个UserValidator的类中,不光对user做了验证,同时还做了初始化,这就是一个副作用

public class UserValidator (
  …
  if ("Valid Password".equals(phrase)) {
    Session.initialize();
    …
  }
  …
)

如果一定要时序性耦合,就应当在函数名称中说明,例如把UserValidator改成checkPasswordAndInitializeSession

5.別重复自己

重复可能是软件中一切邪恶的根源

6.如何写出这样的函数

在写代码时,实在是很难做到以上这几点。但这并不能成为我们写烂代码的借口。但是我们可以这样子做,来保证我们的代码质量。

  • 想什么写什么
  • 然后打磨它
  • 单元测试
  • 分解函数
  • 修改名称
  • 消除重复
  • 保持测试通过

在不久的将来,我们写的每一段代码,都有可能被自己或者别的开发者阅读修改。如果想要给未来的自己或者其他开发者省一点时间,建议在创造代码时一定要考虑到将来读者的感受,一定要让读者看到你的代码时不得不说出赞美的词。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,456评论 5 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,370评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,337评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,583评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,596评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,572评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,936评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,595评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,850评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,601评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,685评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,371评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,951评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,934评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,167评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,636评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,411评论 2 342

推荐阅读更多精彩内容

  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,585评论 18 139
  • 第一部分 打好基础 Laying the Foundation 第一章 欢迎进入软件构建的世界 Welcome t...
    白桦叶阅读 4,597评论 0 17
  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 171,397评论 25 707
  • 传统网站与响应式网站 网络发展最初可以追溯到20世纪50年代,1991年internet开始用于商业用途,随着网络...
    成都用我在线阅读 172评论 0 0
  • 想当初孙悟空在八卦炉里烟熏火燎被烧烤,最终练就火眼金睛。悟空成功后接受采访会怎么说?一万小时理论?因为我很拼...
    那些风阅读 112评论 0 1