代码整洁之道 - 命名

57 阅读13分钟

原文:zhuanlan.zhihu.com/p/663741430?

作为一个程序员,不知道你对整洁的代码有什么理解,我本人在过去很长时间都觉得代码怎么写怎么看着不顺眼,最终学着模仿别人优秀代码,终于有时候可以写出自己满意的代码了。

那么编写整洁代码的程序员就像是艺术家,他能用一些列变换把一块白板变作由 优雅代码 构建的系统。

image.png

那么大家肯定会好奇,什么才是整洁的代码?《代码整洁之道》这本书的作者 Robert C. Martin - Bob 大叔 给了大家答案,建议每个程序员必读,同样的问题 Bob 大叔 分别采访了一些技术大牛们:

Bjarne Stroustrup(C++发明者) 说:

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

Ron Jeffries (《极限编程实施》作者)对整洁代码的理解:

  1. 能通过所有的测试;
  1. 没有重复代码;
  2. 体现系统中的全部设计理念;
  3. 包括尽量少的实体、比如类、方法、函数等。 在以上诸项中,我最在意的是代码重复。如果一段代码重复出现,就表示某种想法未在代码中得到良好的提现。我会尽力去找出到底那是什么,然后再尽力将其更清晰的表达出来。 有意义的命名是体现表达力的一种方式,我往往会修改好几次才会定下名字来,借助 IDE 现代编码工具,重命名的代价极低。除了命名外还会检查对象或者方法是否做的事情过多,如果对象功能多拆分为两个或者更多对象,如果方法功能太多,使用函数抽取重构,从而得到一个较为清晰说明自身功能的方法。

我简单总结下整洁的代码就是:

  1. 看上去非常舒服;
  2. 职责明确,没有多余;
  3. 减少依赖,便于维护;
  4. 高效。

我们在实际开发中经常会发现一些不合理的设计,经常会听到: 这个先这样,以后再说。 勒布朗(LeBlanc)法则 Later equals Never ,以后或许真的到离职那天都没有机会优化曾经的代码。

那么我们为什么要写整洁的代码?

  • 我相信整洁的代码可以提高软件的各种 x-bility(比如健壮性、可读性、可维护性、开发效率性、可扩展性等),有时候开发效率不是仅仅从 0 到 1 的过程,整洁的代码有助于从 1 - 100 的开发效率;
  • 当你写完自己满意的整洁代码时,欣赏它就像一件艺术品一样,满满的成就感。

那么此次主要分享《代码整洁之道》这本书中的第二章节『有意义的命名』(有些点稍加了改造),编程中我们需要给 变量函数参数接口包名目录名文件名 等等命名,那么阅读代码往往是从命名和函数开始,所以我觉得命名是编程中最重要的环节,最简单也是最难的,说简单是因为任何人都可以命名,说难是因为不是任何人都可以做好命名,当你写代码时已经开始纠结哪个命名比较好,为了取个名字使用各种翻译软件找单词,问 ChatGPT,和其他人讨论,那么说明你离整洁的代码不远了。

作为一个程序员如果连命名和函数都写不好,更别要谈什么设计模式架构之类的,那这次就从命名开始吧!

1. 名副其实

选个好名字要花时间,省下的时间比花掉的多,注意命名,一旦发现好的名称,就换掉旧的,这么做, 读你代码的人(包括你自己)会更开心

// Bad
const d;// elapsed time in days 消失的时间,以日计。

const yyyymmdstr = moment().format('YYYY/MM/DD');
// Good
const elapsedTimeInDays;
const daysSinceCreation;
const daysSinceModification;
const fileAgeInDays;

const yearMonthDay = moment().format('YYYY/MM/DD');
// Bad
function getThem(theList: number[]) {
    const list1 = new Array<number>();
    theList.forEach(x => {
        if (x[0] === 4) {
            list1.push(x);
        }
    });
    return list1;
}

2. 避免误导

  1. 避免留下隐藏代码本意的错误线索,比如 vim hp aix sco 都不该作为变量名,因为这是 UNIX 平台专有的名称;
  2. 比如不是 List 类型,就不要用个 accountList 来命名,这样形成误导,表示一组帐号使用 accountGroup 或者 bunchOfAccounts 以及 accounts 好一些;
  3. 提防使用不同之处较小的名称,比如区分 XYXControllerForEfficientHandingOfString XYZControllerForEfficientStoragesOfString ,我们费尽心思才能发现第三个字母 X Z 的区别

以下是误导性的名称最可怕的例子:

const a = l;
if (O === l){
  a = O1;
} else {
  l = 01;
}

你能分的清哪个是 0 哪个是 o,哪个是 l,哪个是 1 吗?

3. 做有意的区分

function copyChars(a1, a2) {
    for (let i = 0; i < a1.length; i++) {
        a2[i] = a1[i];
    }
}

如果把 a1 和 a2 参数名称改为 sourcedestination ,这个函数就会好理解很多。

其次就是废话是另一种没有意义的区分:

const variableCar = {};
const userDataList = [];

class Product {}
class ProductInfo {}
class ProductData {} 
class ProductObject {}

info data object 就和 a an the 一样是意义含混的废话。

废话都是冗余的:

  • Variable 一词永远不应当出现在变量名中
  • Table 一词永远不应当出现在表名中
  • NameString 会比 Name 好吗,难道 Name 会是一个浮点数不成?
  • 如有一个 Customer 的类,又有一个 CustomerObject 的类。是不是就凌乱了。
//  反例
getActiveAccount();
getActiveAccounts();
getActiveAccountInfo();

如果缺少明确约定 ,变量 moneyAmountmoney 没有区别, customerInfocustomer 没有区别。

注意:这里我加了一个前置条件是:缺少明确的约定,比如 PingCode 中我们约定 DTO 传输对象需要以 Info 结尾,数据库实体以 Entity 结尾,那么 Customer CustomerInfo CustomerEntity 就是有意义的区分。

对功能类似的变量名采用统一的命名风格

// Bad
getUserInfo();
getClientData();
getCustomerRecord();

// Good
getUser();

4. 使用读的出来的名称

人类长于记忆使用单词,大脑的相当一部分就是用来容纳和处理单词的,单词可以读的出来。
genymdhmsgen why emm dee aich emm ess , 或者见字照读念 gen-yah-mudda-jims

5. 使用可搜索的名称

单个字母名称 或者 数字常量 是很难在一大篇文章中找出来。

MAX_CLASSES_PRE_STUDENT7 找起来单词肯定更容易,字母 e 是英文中最常用的字母,非常不便于搜索。

  • 名称长短应与其作用域大小相对应
  • 若变量或常量可能在代码中多处使用,应赋予便于搜索的名称。
  • 长名胜于短名称,搜得到的名称胜于自编的名称。
  • 单字母的名称仅用于短方法中的本地变量。
// BAD
for (let i = 0; i < 34; i++) {
    s += (t[j] * 4) / 5;
}

// GOOD
const realDaysPerIdealDay = 4;
const WORK_DAYS_PER_WEEK = 5;
let sum = 0;

for (let j = 0; j < NUMBER_OF_TASKS; j++) {
    const realTaskDays = taskEstimate[j] * realDaysPerIdealDay;
    const realTaskWeeks = realdays / WORK_DAYS_PER_WEEK;
    sum + = realTaskWeeks;
}
// Bad
setTimeout(blastOff, 86400000);

// Good
const MILLISECONDS_IN_A_DAY = 86400000;
setTimeout(blastOff, MILLISECONDS_IN_A_DAY);

可搜索其实就是添加变量说明

// Bad
const cityStateRegex = /^(.+)[,\\s]+(.+?)\s*(\d{5})?$/;
const match = cityStateRegex.match(cityStateRegex)
saveCityState(match[1], match[2]);
// Good
const ADDRESS = 'One Infinite Loop, Cupertino 95014';
const cityStateRegex = /^(.+)[,\\s]+(.+?)\s*(\d{5})?$/;
const match = ADDRESS.match(cityStateRegex)
const city = match[1];
const state = match[2];
saveCityState(city, state);

如下是我们系统中之前存在的一段逻辑代码,要是很快能读懂这段代码简直惊为天人。

return _.all(context.approvers, function (approver) {
  return (approver.ref_type === core.constant.team.memberRefType.user && context.users[approver.ref_id]) 
         || !(denyMyselfRole && approver.ref_type === core.constant.team.memberRefType.role && approver.ref_id === context.myselfRole._id.toString()) 
         || (approver.ref_type === core.constant.team.memberRefType.role && context.roles[approver.ref_id]);
}) ? fn(null, context.approvers) : fn({code: core.status.approvalError.wrongApprover});

6. 避免使用编码

  1. 不要加类型前缀,比如 name , title , users 肯定不需要用 nameString , titleStringusersArray 表示(可能只适用于静态语言);
  2. 成员前缀,不必使用 m_ 前缀来标明成员变量,比如 m_name , m_title , 应该把类和函数做的足够小,消除对成员前缀的需要;
  3. 接口和实现,选择一个硬编码,作者更愿意选择实现

IShapeFactory -> ShapeFactory

CShapeFactory -> ShapeFactoryImpl 目前我们也是通过这种方式在实现类上加 Impl

7. 避免思维映射

不要让读者在脑中把你的名称翻译成他们熟知的名称

比如循环计数器 i j k , 如果你非要取 l m n 就会造成读者思维映射。
如果你记得 r 代表不包含主机名和图式的小写字母版 url 的话,那你真的是太聪明了。

8. 类名

类名应该是 名词或短语 ,像 CustomerWikiPageAccountAddressParser

  • 避免使用 Manager , Processor , Data 或者 Info 这样的类名( 排除约定,有时候也不绝对
  • 类名不应当是动词,这点尤其重要
  • 使用 er 或者 or 作为类名来封装统一的逻辑,比如: ParserResolverDispatcherCalculatorImporterCreatorBuilder

9. 方法名

  • 方法名应该是动词或动词短语,如 postPayment , deletePagesave ,这个类名正好相反
  • 属性访问器、修改器和断言应该根据其值来命名,并加上 getsetis 这些前缀。
const name = employee.getName();
employee.setName("mike");
if (paycheck.isPosted()) {}
  • 名词后加 ze/ize 或者 fy/ify 变成动词
    • tokenizeorganizerealizerobotizememorize
    • beautifysimplifyasyncifypromisifycallbackifyclassifystringifyclarifyverify
  • 重载构造器,使用带参数的静态工厂方法
const operationDescription = OperationDescription.from(operationContext);
const operationDescription = OperationDescription.fromRequest(request);

10. 别抖机灵

如果名称太耍宝,只有和作者一样有幽默感的人才能记住,并且还得记住那个笑话的时候才行, 比如谁知道 HolyHandGrenada 函数是干什么的,没错这个名字挺伶俐,但是不过 DeleteItems 或许是更好的名字。 宁可明确,误为好玩。

  • 别用 whack() 表示 kill()
  • 别用 eatMyShorts() 表示 abort()

11. 每个概念对应一个词,并且一以贯之

  • 不要用 fetchgetretrieve 给多个或者同一个类的同种方法命名。
  • 同一堆代码中的 controllermanagerdriver 会令人困惑。

12. 别用双关语

避免将同一个单词用于不同的目的,同一术语用于不同的概念,基本上就是双关语。

比如说每个类都有一个 add 方法,只要这些 add 方法的参数列表和返回值在语义上等价,就没有问题。

如果多个类中的 add 方法表示通过增加或者链接2个现存值来获取新值。
某个类中是向 collection 追加参数,应该叫 insert 或者 append 更合适。

探讨:对于 CRUD 如何命名更好?

createUserupdateUsergetUsersdeleteUser

addUsermodifyUserremoveUser

13. 使用解决方案的领域名称

只有程序员才会读你的代码,对于熟悉访问者模式的程序员来说,名称 AccountVisitor 更富有意义,哪个程序员不知道 JobQueue 的意思呢。

需要掌握一些常用的软件设计的词汇:

单词解释单词解释
abstract抽象的access存取、访问
action动作binding绑定
blockbinary二进制
adapter适配器aggregation聚合
strategy策略lookup查找
alias别名account帐户
built-in内置的preset预置的
activateactive激活、活动的advanced高级的
mutable可变的immutable不可变的
archive归档assert断言
backup备份batch批量
append追加architecture架构
assign分配assignment分配名词
associated相关attribute属性
immediate立即的,直接的infinite无限的
authentication认证compatible兼容性
checkpoint检查点cleanup清理
definition定义declaration声明
transaction事务compiler编译器
hierarchy层次结构nested嵌套的
recursion递归reference引用
ancestors祖先descendant后代
construct构建dispatch调度、分派、派发
attach附加resolve解析
revoke撤销scope作用域、生存空间
initialize初始化integrate集成
invoke调用lifetime生命周期
ensure确保format格式化
normalize标准化、规范化hook钩子
host宿主manifest清单
snapshot快照wizard向导

参考: wenku.baidu.com/view/d44213…

14. 使用源自所在问题的领域名称

如果实在找不到 使用解决方案的领域名称 , 就采用使用源自所在问题的领域名称,至少,负责维护代码的程序员可以请教领域专家。

15. 添加有意义的语境

比如函数中有 firstName、lastName、street、houseNumber、city、state 和 zipcode 的变量,这个放一起肯定是构建了一个地址,大使如果单独看见 state 呢?所以要添加语境命名为 addressState

16. 不要添加没用的语境

  • 比如在 Ship 子应用中给所有的 Class、Interface、Enum 前都加上 Ship 前缀
  • 对于 Address 类的实体来说, accountAddress 和 customerAddress 都是不错的命名,但是对于类名来说, Address 如果需要和邮政编码 MAC 地址和 Web 地址相区分,最好使用 PostalAddress 、MAC 和 URI 更好

比如我们定义了一个 Team 类,每个成员变量都加 team 前缀就是无意义的语境。

// 没有意义的语境
class Team {
  teamName: string;
  teamId: string;
  teamDescription: string;
}

// Good
class Team {
  name: string;
  id: string;
  description: string;
}

比如在其他地方单独出现了 name 谁知道是 Team Name 呢?这时候需要添加前缀处理 teamName

17. 相似类型的事物如何命名

List、Map、Set 如何区分?

假如这里只有一个数组,使用 usersuserList 或者 userDataList 命名更好:

const users: User[] = [];

如果只有一个 Map,其实也可以使用 users ,因为 JS 有了 Map 类型,但是如果即有 List 又有 Map 呢?

我们可能会加一个 Map 后缀:

const users: User[] = [];
const usersByIdMap: User[] = []; // usersIdMap

假如有一个 Key 是 Team Id,Value 是 Users 数组的 Map,你会如何命名?

  • teamUsers
  • usersByTeamId
  • teams
  • users
  • usersByTeam
  • userByTeamId
  • usersMap
  • userByTeamIdMap

18. 显式优于隐式

const locations = ['Austin', 'New York', 'San Francisco'];
// Bad
locations.forEach((l) => {
  ...
  // l 是什么?
  dispatch(l);
});

// Good
locations.forEach((location) => {
  ...
  dispatch(location);
});

19. 避免无意义的条件判断

// Bad
function createUser(name) {
  let userName;
  if (name) {
    userName = name;
  } else {
    userName = 'why520crazy';
  }
}

// Good
function createUser(name) {
  const userName = name || 'why520crazy';
}

function createUser(name = 'why520crazy') {
  const userName = name;
}

20. 避免使用缩写,特别是不常用的简写

oldVal => oldValue

delUser => deleteUser

err => error

21. 布尔值命名

布尔值(变量和函数)需要带 is, has, can 如:

isBot , isDeleted , hasPermission , canCreateUserisShowisVisible

22 使用正确的时态

已经选择的用户:

  • selectedUsers
  • selectUsers
  • usersSelection(UserSelection)
class UserSelection {
  selectedUsers: User[];
}

可 xxx 的

  • getSelectableProjects
  • editable:是否可编辑的
  • clearable:是否可清除

进行时:

  • isLoading
  • isConnecting
  • isValidating
  • isRunning
  • isListening

最后

取好名字最难的地方在于需要良好的描述技巧和共有文化背景。

  • 写代码的时候多花点时间考虑命名
  • 发现更好的命名使用 IDE 尽快重构
  • 当起了一个满意的名字后会有满满的成就感

希望每个开发者都可以做好命名,如果你过去不是很在意,那么从现在开始用心考虑一下命名吧。