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 個方面增加代碼可讀性和可維護性


分享到:


相關文章: