01.08 从3 个方面增加代码可读性和可维护性

故事

领导:“原来项目有个需求变动,需要你去改一下,没有改很多,这个应该很快吧。”

二盛:“好,我先看一下。”

内心忐忑的二盛打开了那个古老的项目,不看不知道,一看吓一跳,项目的代码大概是这样的:

一文千行洋洋洒洒
每行代码密密麻麻
一气呵成想哪写哪
条理不清一团乱麻

重复代码整齐好看
宁写多次绝不封装
全局变量满天飞翔
想用就用从天而降

变量命名全按顺序
a1 , a2 , a3 , a4
只用for+if语句
实现一切无所畏惧

这里写死那里写死
复用全是直接复制
惜墨如金不写注释
看不懂是别人的事


从3 个方面增加代码可读性和可维护性


从3 个方面增加代码可读性和可维护性


从3 个方面增加代码可读性和可维护性


面对着这些杂乱无章随心所欲的代码,二盛哇的一声哭了出来。

成年人的世界没有容易二字,二盛擦干泪,开始小心翼翼地改代码。

最后90%的时间花在了阅读代码,10%的时间花在修改上,如同完成一个多米诺骨牌的项目,成就感油然而生。

于是二盛在工作汇报中自豪地写下:今天改一个XXX(小功能),备注:原来的代码不好读懂。

当领导看到工作报告时的内心活动:“一个小小的功能,改了那么久?原来的代码是一个新手写的,居然还说不好读懂,看来这个员工能力不行啊。”

前言

  • 文章目的:帮助大家写出可读性和可维护性高的代码
  • 适合人员:初级人员,以及想让队友好好写代码的朋友们
  • 阅读时长:因人而异,总共4000+字,看不完点个收藏⭐

3 个方面

准备数据

<code>// 鼠笼
const mouseList = [
{ id: 'm01', name: '小白鼠', type: '0' },
{ id: 'm02', name: '小黑鼠', type: '4' },
{ id: 'm03', name: '小红鼠', type: '5' },
{ id: 'm04', name: '小橙鼠', type: '3' },
{ id: 'm05', name: '小黄鼠', type: '1' },
{ id: 'm06', name: '小青鼠', type: '1' },
{ id: 'm07', name: '小蓝鼠', type: '2' },

{ id: 'm08', name: '小绿鼠', type: '2', cap:{} },
{ id: 'm09', name: '小紫鼠', type: '5' },
]
// 类型对照表
const typeMap = {
'0': '家鼠',
'1': '田鼠',
'2': '竹鼠',
'3': '松鼠',
'4': '米老鼠',
'5': '快乐番薯'
}
复制代码/<code>

第1方面:表明意图

明确告诉读代码的人你在干什么

下面是例子

需求

你去委婉的告诉小绿鼠,他老婆出轨了。

✍实现

<code>var flag = false
for (var i = 0; i < mouseList.length; i++) {
if (mouseList[i].name == '小绿鼠') {
mouseList[i].cap.color = 'green'
flag = true
break
}
}
if (flag) {
console.log('我已经告诉他了')
} else {
console.warn('没有找到小绿鼠')
}
复制代码/<code>

分析

现在我们来分析以上代码做的事情:

  1. 定义一个变量flag, 默认值为false
  2. 对mouseList进行遍历
  3. 如果数组元素的name是小绿鼠
  4. 给该数组元素的cap属性的color属性赋值
  5. 把变量flag重新赋值true
  6. 终止循环
  7. 通过flag判断是否找到,给出提示

可以看到,我们需要看完一整段代码之后,才能知道代码是在做什么,因为第一眼看到的是for+if,我们只能由此得知要进行遍历+判断,而无法得知更明确的意图。

优化

下面这3种方法可以让代码的意图更加明确:

  1. 直接写注释

在两小段代码开通分别添加注释:

  • 在所有老鼠中找到一只名字叫小绿鼠的老鼠,帮他把帽子染成绿色
  • 事情做完了给出提示

写注释简单粗暴,可是十分有效。然而无论是作者写注释,还是读者读注释,都需要耗费时间,因此如果是简单的功能,那么注释是没有必要的,好钢用在刀刃上,注释也应该写在关键之处。

  1. 命名里面给讯息

注释可以省略不写,但是命名一般跑不掉,正所谓命名不规范,队友两行泪,瞎起名伤害的不仅仅是队友,还有将来看代码的自己。

原代码的写法是立一个flag

<code>var flag = false
复制代码/<code>

现在我们把它改成这样

<code>let isFound = false
复制代码/<code>

这样写有3个好处:

  1. 用ES6的let而非const,说明我将来要对这个变量重新赋值,而var只是单纯声明
  2. is开头说明变量是Boolean类型,如果在下文中更一群杂七杂八的变量混在一起,也能一眼认出个大概
  3. isFound的意思是是否找到,这个found一出来,读者马上就知道作者找东西的意图

found 是 find 的过去分词

所以,单看let isFound = false,不看下面的代码,我们就可以推测出作者是在寻找目标,isFound是作为是否找到目标的标识,如果找到目标以后,一定会有isFound = true的代码出现

  1. 使用函数

这里使用函数的意思是,把非必要内容封装进函数中,只留下主要信息,通过主要信息来凸显意图。

封装之前我们先把代码逻辑再拆分细一些, 把寻找目标对目标进行操作分成两步,下面把

寻找目标封装成函数,首先提取要素:

  • 范围 (在哪里找)
  • 目标描述 (找啥样的)
  • 数量 (找几个)
  • 结果 (找到没)

以此为来封装函数

<code>/**
* 数组里面找元素
* @param {array} array 范围
* @param {function} callback 目标描述
* @param {number} count 数量
* @return {array} 结果
*/
function arrayFindItem(array, callback, count) {
const result = []
let _count = 0
for (let i = 0; i < array.length; i++) {
if (callback(array[i])) {
_count++
result.push(array[i])
if (_count === count) {
return result
}
}
}
return result
}
复制代码/<code>

接着使用它

<code>const result = arrayFindItem(mouseList, function(mouse) {
return mouse.name === '小绿鼠'
}, 1)

复制代码/<code>

这样一来,代码里面剩下部分的信息就很明确了:

  • 行为: arrayFindItem 在数组中找元素
  • 在哪里找:mouseList
  • 找啥样的:function(mouse) { return mouse.name === '小绿鼠' }
  • 找几个: 1
  • 找到没: result

虽然清晰了不少,但前提是需要把arrayFindItem的参数和返回值了解清楚,而就本例而言,有更好的解决方法: Array.prototype.find

<code>const result = mouseList.find(function(mouse) {
return mouse.name === '小绿鼠'
})
复制代码/<code>

由于是es6规范里的数组方法,所以大家对它的行为已经非常了解,不需要额外的阅读成本。


第2方面:代码拆分

事情要一件一件地做,代码要一块一块地写。

需求

  1. 把笼子里面生的鼠,并按照下面的做法烹饪一下:竹鼠 -> 油炸家鼠和田鼠 -> 水煮番薯 -> 碳烤
  2. 做成晚餐,我晚上要吃

✍实现

新手可能写出来的代码

<code>var dinnerList = []
for (var i = 0; i < mouseList.length; i++) {
if (mouseList[i].isRaw != true) {
if (mouseList[i].type === '2') {
// 宽油竹鼠
mouseList[i].recipe = '油炸配方'
mouseList[i].newName = '油炸' + mouseList[i].name
// ...被省略的油炸的其他操作
mouseList[i].isRaw = true
dinnerList.push(mouseList[i])
} else if (mouseList[i].type === '0' || mouseList[i].type === '1') {
// 水煮家鼠 + 田鼠
mouseList[i].recipe = '水煮配方'
mouseList[i].newName = '水煮' + mouseList[i].name
// ...被省略的水煮的其他操作
mouseList[i].isRaw = true
dinnerList.push(mouseList[i])
} else if (mouseList[i].type === '5') {
// 烤番薯
mouseList[i].recipe = '碳烤配方'
mouseList[i].newName = '碳烤' + mouseList[i].name
// ...被省略的碳烤的其他操作
mouseList[i].isRaw = true
dinnerList.push(mouseList[i])
}
}
}
console.log(dinnerList)
复制代码/<code>

分析

  1. 定义数组dinnerList
  2. 遍历mouseList
  3. 找出所有属性isRaw是true的数组元素
  4. 在3的基础上,根据属性 type的不同,进行不同的操作type 是 2 ==> 油炸操作type 是 0或1 ==> 水煮操作type 是 5 ==> 碳烤操作

很明显,把烹饪过程直接写在for循环里面会造成循环过长,不利于阅读,所以应该将其拆分出来。

优化

那么我们先进行第一步,将烹饪方法拆分出来

<code>/* ****** 这里是烹饪的方法们 ****** */

function fry(mouse) {
mouse.recipe = '油炸配方'
mouse.newName = '油炸' + mouse.name
// ...被省略的油炸的其他操作
mouse.isRaw = true
}
function boil(mouse) {
mouse.recipe = '水煮配方'
mouse.newName = '水煮' + mouse.name
// ...被省略的水煮的其他操作
mouse.isRaw = true
}
function roast(mouse) {

mouse.recipe = '碳烤配方'
mouse.newName = '碳烤' + mouse.name
// ...被省略的碳烤的其他操作
mouse.isRaw = true
}

复制代码/<code>

这样我们就有3个烹饪方法了,把它们放一起给上注释,既清晰又方便维护。

假设现在的需求是修改某一种烹饪方法,我们只需要找到方法,并修改方法内部的实现就搞定了,甚至不用去管该方法在哪里被调用。

还没完,接着把代码补充完整

<code>var dinnerList = []
for (var i = 0; i < mouseList.length; i++) {
if (mouseList[i].isRaw != true) {
if (mouseList[i].type === '2') {
fry(mouseList[i]) // 宽油竹鼠
dinnerList.push(mouseList[i])
} else if (mouseList[i].type === '0' || mouseList[i].type === '1') {
boil(mouseList[i]) // 水煮家鼠 + 田鼠
dinnerList.push(mouseList[i])
} else if (mouseList[i].type === '5') {
roast(mouseList[i]) // 烤番薯
dinnerList.push(mouseList[i])
}
}
}
console.log(dinnerList)
复制代码/<code>

这段循环中出现了if... else if... else if...,并且判断的对象都是type,这就证明了里面有可以拆分出来的逻辑。

回顾一下我们最开始的需求

  • 竹鼠 -> 宽油竹鼠
  • 家鼠和田鼠 -> 水煮
  • 番薯 -> 碳烤

让一段代码的逻辑贴近需求,那么无论是代码可读性还是应对需求变更的能力,都会上升一个层次。

接下来就是把需求转换成代码: 左边用类型代替,右边用函数代替,初步版:

条件操作type 是 2油炸操作type 是 0或1水煮操作type 是 5碳烤操作

接着用代码符号代替:

typecookFn2fry()0 || 1boil()5roast()

到这一步会发现这种对映关系就是key => value,key是老鼠的type, value是烹饪的方法cookFn, 所以我们理所应当用对象来存储对应关系

<code>// 老鼠烹饪方法映射表
const mouseCookFnMap = {
// type: cookFn
'0': boil,

'1': boil,
'2': fry,
'5': roast
}
复制代码/<code>

完美!你如果有强迫症的话,也可以把所有的类型都补充完整,像这样

<code>// 老鼠烹饪方法映射表(强迫症版)
const mouseCookFnMapIllVer = {
// type: cookFn
'0': boil,
'1': boil,
'2': fry,
'3': undefined, // 未指定烹饪方法
'4': undefined,
'5': roast
}
复制代码/<code>

写好了就马上用一下

<code>var dinnerList = []
for (var i = 0; i < mouseList.length; i++) {
if (mouseList[i].isRaw != true) {
var cookFn = mouseCookFnMap[mouseList[i].type]
if (cookFn) { // cookFn !== undefined
cookFn(mouseList[i])
dinnerList.push(mouseList[i])
}
}
}
console.log(dinnerList)
复制代码/<code>

这时候,你收到一个需求变动:“宽油竹鼠太费油了,给我改成碳烤。”

只需要把'2': fry,改成'2': roast,就了。

并且,使用映射关系表可以轻松应对某些需求更为复杂的场景。

比如说宽油竹鼠,实际上并非油炸就能完成的,竹鼠的皮肉比较厚实,还需要长时间的焖煮,所以对于竹鼠,需要先油炸再焖煮。

此时,映射表的value就不单单是一个方法了,应该是多个方法并且是有序的,显然可以用数组来存储:

<code>// 加一个`焖煮方法`
function braise(mouse) {
// ... 省略的焖煮方法具体实现
}

// 老鼠烹饪方法映射表加强版
const mouseCookFnMapPlus = {
// type: cookFnArray
'0': [boil],
'1': [boil],
'2': [fry, braise],
'5': [roast]
}
复制代码/<code>

使用的时候将直接调用方法

<code>cookFn(mouseList[i])
复制代码/<code>

改成遍历数组依次调用

<code>cookFnArray.forEach(cookFn => cookFn(mouseList(i)))
复制代码/<code>

这里实在不想写for循环了,用了forEach,下文会劝你们不要尽量写for循环


第3方面:去除冗余

这一方面主要是从语法层面上,来探讨如何去掉代码中的冗余,具体做法是找到代码中与主题无关或重复的部分(主要是变量),尝试去除它们。

这里就不加新的需求了,直接把上面的例子拿过来用

箭头函数➡

ES6箭头函数的优点有两个:

  1. 改变函数内this指向

过去为了将函数内部的this指向到外层作用域,主要方法是

<code>var that = this
// or
var self = this
复制代码/<code>

讲真的,看到that我头都大了,每个函数开始前都定义一个that不累吗?

而箭头函数中的this就是指向到外层的,彻底去除了上面这种冗余的代码!

能写=>的时候,就不要写function。与其说用箭头函数是为了将this指向到外层,不如说function关键字是为了将this指向到本层才会去用。

  1. 简化写法

先感受一下

<code>mouseList.find(function(mouse) {
return mouse.name === '小绿鼠'
})
// 箭头函数写法
mouseList.find(mouse => mouse.name === '小绿鼠')
复制代码/<code>

少写很多字有没有,附上写法对比

写法function(参数)=>{自动return函数体}原写法function(参数){函数体}箭头1(参数)=>{函数体}箭头2(参数)=>// 有单行代码

箭头函数太棒了!写者能少写,看者能少看。具体使用看文档,我们接着往下看

循环

首先看这个for循环,它又长又宽

<code>for (var i = 0; i < mouseList.length; i++) {
if (mouseList[i].name == '小绿鼠') {
mouseList[i].cap.color = 'green'
}
}

复制代码/<code>

很明显此处的变量i毫无意义,那么如何去除i呢?

第一种是使用ES6的 for..of

<code>for (const mouse of mouseList) {
if (mouse.name == '小绿鼠') {
mouse.cap.color = 'green'
}
}
复制代码/<code>

不过使用for...of,即使想要下标 i 它也给不了,推荐一般情况下,能用 forEach 的时候都用Array.prototype.forEach()

<code>mouseList.forEach((mouse) => {
if (mouse.name == '小绿鼠') {
mouse.cap.color = 'green'
}
})
复制代码/<code>

说明:for...of可以遍历所有部署了iterator(迭代器)的数据,而forEach仅仅是数组原型上的方法。但是你如果铁了心要用forEach,可以利用展开运算符(...)来把可迭代对象转换成数组:[...iterableValue].forEach()

forEach虽然好用,但是千万别只用forEach用到死,数组还有那么多好用的方法,它们封装得更完整也更具语义化,对数组方法不熟悉的话可以多看几遍文档

知道有写着方法,一直想不起来去用怎么办?

在写循环之前,先想想自己最后想要什么,有了明确的目标之后再下手

目标手段返回值找一项find数组元素 (没找到是undefined)找一项的下标findIndexnumber(没找到是-1)找多个(过滤)filterarray复制全部并改造maparray有部分是?someboolean全都是?everyboolean.........

解构(析构)

变量的解构赋值(destructuring)

变量的解构有很多种,都差不多,这里只介绍最常用的一种,对象解构

例子

假设我们要取出小白鼠的几个属性,不用解构赋值是这样的

<code>const whiteMouse = { id: 'm01', name: '小白鼠', type: '0' }
const id = whiteMouse.id
const name = whiteMouse.name
const type = whiteMouse.type
复制代码/<code>

用了解构赋值是这样的

<code>const whiteMouse = { id: 'm01', name: '小白鼠', type: '0' }
const { id, name, type } = whiteMouse
复制代码/<code>

优势很明显了,去掉了很多冗余的代码。

解构可以用在很多地方,只要是取对象的某个属性赋值给一个变量,就可以用解构,下面是小绿鼠的例子的加强版

<code>const greenMouse = mouseList.find(mouse => mouse.name === '小绿鼠')
if (greenMouse) {
greenMouse.cap.color = 'green'
greenMouse.cap.size = 'big'
greenMouse.cap.brightness = 'high'
}
复制代码/<code>

由于担心太过委婉以致于小绿鼠没有发觉,我们增大了帽子尺寸并且让帽子变得更加耀眼。

在遇到这种一个对象属性在后文中被多次使用的情况,最好用一个变量来存一下,避免多个 对象.属性.属性... 的写法让代码臃肿不堪,影响阅读。

<code>const greenMouse = mouseList.find(({ name }) => name === '小绿鼠')
if (greenMouse) {
const { cap } = greenMouse
cap.color = 'green'
cap.size = 'big'
cap.brightness = 'high'
}
复制代码/<code>

好理解也好用,不过细心的话会发现这里还有另一个地方也用了解构,就是.find()的回调函数的参数部分。

这样写可以减少一个自定义的变量mouse,它也是与主题无关的,而且定义出来只用一次,也算一种冗余。

解释一下这个解构

<code>(mouse) => mouse.name === '小绿鼠'
复制代码/<code>

.find()传入的回调函数(mouse) => mouse.name === '小绿鼠', 它的第一个参数mouse,是mouseList中的元素,也就是

<code>   { id: 'm01', name: '小白鼠', type: '0' } // 第一次回调运行时`mouse`的值
{ id: 'm02', name: '小黑鼠', type: '4' } // 第二次回调运行时`mouse`的值
// ...
复制代码/<code>

既然mouse是一个对象,我们只需要它的name属性,那就({ name })只不过是把

<code>const mouse = { id: 'm01', name: '小白鼠', type: '0' }
复制代码/<code>

改成了

<code>const { name } = { id: 'm01', name: '小白鼠', type: '0' }
复制代码/<code>

解构真的很常用,请求接口回来的时候,就经常会这么写

<code>async function getData() {
const { code, msg, data } = await requestFn()
// ...

}
复制代码/<code>

所以这里说一句,请求接口后把回调函数和.then()收起来吧,你看这async + await,它不清晰吗,可读性不高吗?

码农三哥,一名普通程序员,会点java软件开发,对AI人工智能有点兴趣,后续会每日分享些关于互联网技术方面的文章,感兴趣的朋友可以关注我,一起交流学习。

想转型或刚步入程序员Java开发的朋友,有问题可以留言或私信我!


从3 个方面增加代码可读性和可维护性


分享到:


相關文章: