原文:zhuanlan.zhihu.com/p/663741430?
作为一个程序员,不知道你对整洁的代码有什么理解,我本人在过去很长时间都觉得代码怎么写怎么看着不顺眼,最终学着模仿别人优秀代码,终于有时候可以写出自己满意的代码了。
那么编写整洁代码的程序员就像是艺术家,他能用一些列变换把一块白板变作由 优雅代码 构建的系统。
那么大家肯定会好奇,什么才是整洁的代码?《代码整洁之道》这本书的作者 Robert C. Martin - Bob 大叔 给了大家答案,建议每个程序员必读,同样的问题 Bob 大叔 分别采访了一些技术大牛们:
Bjarne Stroustrup(C++发明者) 说:
“我喜欢优雅和高效的代码,代码逻辑应当直接了当,叫缺陷难以隐藏;尽量减少依赖关系,使之便于维护;依据某种分层战略完善错误处理代码;性能调至最优,省得引诱别人做没有必要的优化,搞出一堆混乱来,整洁的代码之做好一件事。”
Ron Jeffries (《极限编程实施》作者)对整洁代码的理解:
- 能通过所有的测试;
- 没有重复代码;
- 体现系统中的全部设计理念;
- 包括尽量少的实体、比如类、方法、函数等。 在以上诸项中,我最在意的是代码重复。如果一段代码重复出现,就表示某种想法未在代码中得到良好的提现。我会尽力去找出到底那是什么,然后再尽力将其更清晰的表达出来。 有意义的命名是体现表达力的一种方式,我往往会修改好几次才会定下名字来,借助 IDE 现代编码工具,重命名的代价极低。除了命名外还会检查对象或者方法是否做的事情过多,如果对象功能多拆分为两个或者更多对象,如果方法功能太多,使用函数抽取重构,从而得到一个较为清晰说明自身功能的方法。
我简单总结下整洁的代码就是:
- 看上去非常舒服;
- 职责明确,没有多余;
- 减少依赖,便于维护;
- 高效。
我们在实际开发中经常会发现一些不合理的设计,经常会听到: 这个先这样,以后再说。 勒布朗(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. 避免误导
- 避免留下隐藏代码本意的错误线索,比如
vim
hp
aix
sco
都不该作为变量名,因为这是 UNIX 平台专有的名称; - 比如不是 List 类型,就不要用个
accountList
来命名,这样形成误导,表示一组帐号使用accountGroup
或者bunchOfAccounts
以及accounts
好一些; - 提防使用不同之处较小的名称,比如区分
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 参数名称改为 source
和 destination
,这个函数就会好理解很多。
其次就是废话是另一种没有意义的区分:
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();
如果缺少明确约定 ,变量 moneyAmount
和 money
没有区别, customerInfo
和 customer
没有区别。
注意:这里我加了一个前置条件是:缺少明确的约定,比如 PingCode 中我们约定 DTO 传输对象需要以 Info 结尾,数据库实体以 Entity 结尾,那么
Customer
CustomerInfo
CustomerEntity
就是有意义的区分。
对功能类似的变量名采用统一的命名风格 :
// Bad
getUserInfo();
getClientData();
getCustomerRecord();
// Good
getUser();
4. 使用读的出来的名称
人类长于记忆使用单词,大脑的相当一部分就是用来容纳和处理单词的,单词可以读的出来。
genymdhms
读 gen why emm dee aich emm ess
, 或者见字照读念 gen-yah-mudda-jims
5. 使用可搜索的名称
单个字母名称
或者 数字常量
是很难在一大篇文章中找出来。
MAX_CLASSES_PRE_STUDENT
和 7
找起来单词肯定更容易,字母 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. 避免使用编码
- 不要加类型前缀,比如
name
,title
,users
肯定不需要用nameString
,titleString
,usersArray
表示(可能只适用于静态语言); - 成员前缀,不必使用
m_
前缀来标明成员变量,比如m_name
,m_title
, 应该把类和函数做的足够小,消除对成员前缀的需要; - 接口和实现,选择一个硬编码,作者更愿意选择实现
IShapeFactory
-> ShapeFactory
❌
CShapeFactory
-> ShapeFactoryImpl
目前我们也是通过这种方式在实现类上加 Impl
7. 避免思维映射
不要让读者在脑中把你的名称翻译成他们熟知的名称
比如循环计数器 i
j
k
, 如果你非要取 l
m
n
就会造成读者思维映射。
如果你记得 r
代表不包含主机名和图式的小写字母版 url
的话,那你真的是太聪明了。
8. 类名
类名应该是 名词或短语 ,像 Customer
、 WikiPage
、 Account
和 AddressParser
- 避免使用
Manager
,Processor
,Data
或者Info
这样的类名( 排除约定,有时候也不绝对 ) - 类名不应当是动词,这点尤其重要
- 使用 er 或者 or 作为类名来封装统一的逻辑,比如:
Parser
、Resolver
、Dispatcher
、Calculator
、Importer
、Creator
、Builder
9. 方法名
- 方法名应该是动词或动词短语,如
postPayment
,deletePage
或save
,这个类名正好相反 - 属性访问器、修改器和断言应该根据其值来命名,并加上
get
,set
,is
这些前缀。
const name = employee.getName();
employee.setName("mike");
if (paycheck.isPosted()) {}
- 名词后加
ze/ize
或者fy/ify
变成动词tokenize
、organize
、realize
、robotize
、memorize
beautify
、simplify
、asyncify
、promisify
、callbackify
、classify
、stringify
、clarify
、verify
- 重载构造器,使用带参数的静态工厂方法
const operationDescription = OperationDescription.from(operationContext);
const operationDescription = OperationDescription.fromRequest(request);
10. 别抖机灵
如果名称太耍宝,只有和作者一样有幽默感的人才能记住,并且还得记住那个笑话的时候才行, 比如谁知道 HolyHandGrenada
函数是干什么的,没错这个名字挺伶俐,但是不过 DeleteItems
或许是更好的名字。 宁可明确,误为好玩。
- 别用
whack()
表示kill()
; - 别用
eatMyShorts()
表示abort()
11. 每个概念对应一个词,并且一以贯之
- 不要用
fetch
、get
、retrieve
给多个或者同一个类的同种方法命名。 - 同一堆代码中的
controller
、manager
、driver
会令人困惑。
12. 别用双关语
避免将同一个单词用于不同的目的,同一术语用于不同的概念,基本上就是双关语。
比如说每个类都有一个 add
方法,只要这些 add
方法的参数列表和返回值在语义上等价,就没有问题。
如果多个类中的 add
方法表示通过增加或者链接2个现存值来获取新值。
某个类中是向 collection
追加参数,应该叫 insert
或者 append
更合适。
探讨:对于 CRUD 如何命名更好?
createUser
、 updateUser
、 getUsers
、 deleteUser
addUser
、 modifyUser
、 removeUser
13. 使用解决方案的领域名称
只有程序员才会读你的代码,对于熟悉访问者模式的程序员来说,名称 AccountVisitor
更富有意义,哪个程序员不知道 JobQueue
的意思呢。
需要掌握一些常用的软件设计的词汇:
单词 | 解释 | 单词 | 解释 |
---|---|---|---|
abstract | 抽象的 | access | 存取、访问 |
action | 动作 | binding | 绑定 |
block | 块 | binary | 二进制 |
adapter | 适配器 | aggregation | 聚合 |
strategy | 策略 | lookup | 查找 |
alias | 别名 | account | 帐户 |
built-in | 内置的 | preset | 预置的 |
activate 、 active | 激活、活动的 | 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 如何区分?
假如这里只有一个数组,使用 users
比 userList
或者 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
, canCreateUser
、 isShow
、 isVisible
22 使用正确的时态
已经选择的用户:
- selectedUsers
- selectUsers
- usersSelection(UserSelection)
class UserSelection {
selectedUsers: User[];
}
可 xxx 的
- getSelectableProjects
- editable:是否可编辑的
- clearable:是否可清除
进行时:
- isLoading
- isConnecting
- isValidating
- isRunning
- isListening
最后
取好名字最难的地方在于需要良好的描述技巧和共有文化背景。
- 写代码的时候多花点时间考虑命名
- 发现更好的命名使用 IDE 尽快重构
- 当起了一个满意的名字后会有满满的成就感
希望每个开发者都可以做好命名,如果你过去不是很在意,那么从现在开始用心考虑一下命名吧。