序
最近被领导安排,参加了几次codeReview,发现了不少不大不小的问题。因为是另一个小组的,所以我基本上没怎么关注业务逻辑,主要重点放在了对代码风格、抽象层次等的关注,随之发现了一堆问题。
正好我最近在看《代码整洁之道》
这本书,因此我有了整理这方面的一些想法。
本系列将分为上中下三篇,分别从纯理论
、方向
、实践
层面来对代码整洁之道
进行个人的一些见解说明。
代码
在第一篇大概介绍了一下关于代码整洁程度的重要性、如何保持代码的整洁等几个宏观的方面。第二篇,我们将视角放眼于各个应该注意的方面,并对其进行了描述。
在本篇,我们将会针对第二篇所说的那些重点,来进行代码层面的概述,给大家更为直观的体现。(所选语言为javascript
)
回顾一下上一篇所关注的重点
- 命名
- 函数
- 注释
- 代码格式
- 错误处理
- 边界
- 单元测试
- 迭进设计
- 逐步改进
命名
- 提防使用外形相似度较高的名称
// bad
const isLoading1 = false
const isLoading2 = false
// good
const isLoadingForToast = false
const isloadingForTable = false
复制代码
- 使用可以读的出来的名称,而不是某种简写
// bad
function test(a, b, c) {
doSomeThing
}
// good
function test(name, age, sex) {
doSomeThing
}
复制代码
对于函数的形参,两种命名所造成的后期维护压力差别是巨大的。
试想一下,你要接手别人的代码,别人的代码中,参数命名尽是
a
、b
、c
这种毫无表述意义的标识符,得有多绝望。如果命名可以很好的表达清楚所代表的意思,那么不管是对于自己的开发,还是别人的维护,都有足够大的好处。
- 使用可以搜索的名称,比如定义常量
// bad
if (obj.contentType === 'application/json') {
xxx
}
a.contentType = 'application/json'
// good
if (obj.contentType === ContentType.APPLICATION_JSON) {
xxx
}
a.contentType = ContentType.APPLICATION_JSON
复制代码
对于这种不会进行改变的值,直接使用常量进行声明,维护统一的一个值,而不是在需要修改的时候满世界的找哪儿使用了。
函数
-
函数应该尽量短小
-
函数应该遵循
单一职责原则
,也就是函数应该只做一件事,做好这件事,并且只做这一件事
// bad
async function init() {
const name = await getName()
// doSomethingForName...
const age = await getAge()
// doSomethingForAge...
const sex = await getSex()
// doSomethingForSex...
}
// good
async function changeName() {
const name = await getName()
// doSomethingForName...
}
async function changeAge() {
const age = await getAge()
// doSomethingForAge...
}
async function changeSex() {
const sex = await getSex()
// doSomethingForSex...
}
function init() {
changeName()
changeAge()
changeSex()
}
复制代码
函数只做一件事是为了更好的解耦程序。
在
bad
示例中,init
函数里面混杂着name
、age
、sex
三种逻辑,全部混杂在一起,不仅函数体积过大,对于维护来说,不仅要考虑三种逻辑的边界,还得考虑他们是否存在某种关联。而
good
示例中,将name
、age
、sex
三种逻辑进行拆分,分别拆分成为三个独立的函数,各自负责自己相关的逻辑,init
函数只负责调用它们。不仅方便自己进行逻辑的解耦,更方便后来的维护者对逻辑的梳理。
- 不要害怕长名称,长而具有描述性的名称,要比短而令人费解的短名称好,要比描述性的长注释好
// bad
// 这是一个用于用于判断是否是女性的函数
function itpaw(person) {
return person.sex === 'woman'
}
// good
function isThisPersonAreWoman(person) {
return person.sex === 'woman'
}
复制代码
bad
示例的名称是一个简称,作者可能为了防止别人看不懂,或者防止自己也忘了这个的意思,增加了一条用于描述的注释,可是即使有注释,别人在其他地方进行引用的时候,也需要思索半天那个函数叫啥。反之good
示例直接使用了代表他功能的函数名称,既不需要增加额外的注释,又能让人有印象,可以很快的想起。
- 函数的复杂度往往与参数的个数成正比,因此函数参数应该尽可能的减少,不要多于三个,多余三个应当改为使用对象传参或者考虑是否应该拆分
// bad
function add(name, age, sex, height, hobby) {
doSomeThing...
}
// good
function add(person) {
const { name, age, sex, height, hobby } = person
doSomeThing...
}
复制代码
函数的参数是函数对外暴露的一个重要的点,如果参数的数量过多,可能会提高使用者的心智负担,因此尽量减少函数的参数,对于函数的封装,往往是很有必要的。
- 尽量使用
纯函数
,纯函数具有更好的可维护性,更健壮
// bad
let age = 12
function fn() {
doSomeThing(age)
}
// good
function fn(age) {
doSomeThing(age)
}
复制代码
bad
示例中,fn函数内部引用了暴露在外的age
属性,这是不安全的,因为你不知道是否还有其他更改age
的可能,对于这种情况来说,doSomeThing
是无法掌控的。而在
good
示例,doSomeThing
所引用的age
,是通过fn
函数传递进来的,也就是我们不需要考虑其他更改age
的可能,我们的目标只关注于age
。
注释
注释更多的在于个人的把握,我见过好多类库声明几个属性做了上百行的注释,也见过这些类库很复杂的逻辑操作一点注释不进行添加的,对于注释,更多需要的可能还是对于重点的关注。
代码格式
- 代码块之间留行分割
// bad
function fn() {
const b = 1
if (xxx) {
xxxxx
}
c.name = 123
}
// good
function fn() {
const b = 1
if (xxx) {
xxxxx
}
c.name = 123
}
复制代码
对于代码的可读性来说,代码块或者逻辑直接进行留白,是极为重要的。
- 关系密切的概念应该互相靠近
- 变量声明尽可能靠近其使用位置
// bad
export default {
data() {
return {
a: 1,
b: 2
}
},
methods: {
changeA() {
this.a = 2
},
changeB() {
this.b = 1
}
},
mounted() {
this.changeA()
}
}
// good
function someThingWithA() {
const a = ref(1)
const changeA = () => a.value = 2
onMounted(changeA)
return {
a,
changeA
}
}
function someThingWithB() {
const b = ref(2)
const changeB = () => b.value = 1
return {
b,
changeB
}
}
export default {
setup() {
const { a, changeA } = someThingWithA()
const { b, changeB } = someThingWithB()
return {
a,
changeA,
b,
changeB
}
}
}
复制代码
这个代码块,我使用了
vue
来举例子
bad
示例中的代码片段是vue2.0
的,options
风格决定了他的所有逻辑只能使用固定的写法,写在data
、methods
、mounted
等规划好的模块中,但是这也决定了他无法解耦。a
相关的逻辑分在data
、methods
、mounted
三个模块中,b
相关的逻辑分在data
、methods
两个模块中,而a
和b
的模块还必须紧密的写在一起,造成维护以及理解上的困难。
good
示例中的代码,是vue3.0
的,composable
的风格,使用函数,将每个功能进行拆分,最后统一进行处理,someThingWithA
只负责a
相关逻辑,someThingWithB
只负责b
相关逻辑,达到更好的解耦,便于理解、维护。备注:可能有点跑偏,这并不算是代码格式的问题,只是为了演示一下可能会出现的情况。
关于这块,可以去看看大帅老猿的这篇文章juejin.cn/post/689054…,里面很详细的介绍了
composition-api
相对于options-api
的变化
- 若某个函数调用了另一个,就应该把他们放到一起,而且调用者应尽可能放在被调用者上面
// bad
function c() {
d()
}
function a() {
b()
}
function d() {
console.log(123)
}
function b() {
c()
}
// good
function a() {
b()
}
function b() {
c()
}
function c() {
d()
}
function d() {
console.log(123)
}
复制代码
- 使用短行代码(最多80列或120列)
这个实际上就是一个功能的拆分,将一个大函数,拆分成颗粒度细微的小函数,便于维护。
- 运算符之间应该存在空格
// bad
const a=1
a+=1
a**=2
// good
const a = 1
a += 1
a **= 2
复制代码
- 应该使用统一的缩进
// bad
function a()
{
if(xxx) {
xxx
}
}
// good
function a() {
if (xxx) {
xxx
}
}
复制代码
- 团队应该使用统一的代码风格
这点是最最最重要的!!!
这点是最最最重要的!!!
这点是最最最重要的!!!
错误处理
- 使用统一的错误处理模块
- 对错误进行分类处理
- 对各种错误信息应该有着详尽的描述
// bad
async function getName() {
try {
xxxx
} catch (e) {
if (e.code === 400) {
xxx
} else if (e.code === 500) {
xxx
}
}
}
// good
// catchError.js
const networkErrorMap = new Map([
[404, 'not found'],
[500, 'server error'],
xxxx
])
function catchNetworkError(networkError) {
let errMsg = networkErrorMap.get(networkError.code)
if (!errMsg) {
errMsg = '未知错误!'
}
return errMsg
}
export function catchError(e, cb) {
let msg = ''
switch(e.type) {
case 'networkError':
msg = catchNetworkError(e)
break
case 'xxx'
xxx
}
toast(msg)
if (isFunction(cb)) {
cb(e)
}
}
// index.js
import { catchError } from 'catchError.js'
function doSomeThing(err) {
xxx
}
async function getName() {
get().catch(e => {
catchError(e, doSomeThing)
})
}
复制代码
小结
其实那些要点,光代码层面的,大概也就这么多,后面的边界
、单元测试
、迭进设计
、逐步改进
之类的,都更偏向于约定以及习惯了,在这里,就不展开了。
代码的整洁,不管对于个人,还是对于团队,都是相当重要的一环。
而怎么保持,就需要各位自行努力了。