隨著軟件應用的復雜度不斷上升,為了確保應用穩定且易拓展,代碼質量就變的越來越重要。
不幸的是,包括我在內的幾乎每個開發者在職業生涯中都會面對質量很差的代碼。這些代碼通常有以下特征:
這些話聽起來非常常見:“我不明白這部分代碼怎么工作的”,“這代碼太爛了”,“這代碼太難改了”等等。
有一次我現在的同事因為在之前的團隊處理過難以維護的Ruby 編寫的 REST API 而辭職,他是接手了之前開發團隊的工作。在修復現有的 bug 時會創造新的 bug,添加新的特性也會創造一系列新的 bug,而客戶也不想以更好的設計去重構應用,因而我的同事做了辭職這個正確的決定。
這樣的場景時有發生,我們能做些什么呢?需要牢記于心的是:僅僅讓應用可以運行和關注代碼質量是不同的。一方面你需要滿足應用的功能,另一方面你需要花時間確認是否任意的函數沒有包含太多職責、是否所有函數都使用了易理解的變量和函數名并且是否避免了函數的副作用。
函數(包括對象的方法)是讓應用運行的小齒輪。首先你應該專注于它們的結構和編寫,而下面這篇文章闡述了編寫清晰易懂且容易測試的函數的佳實踐。
要避免編寫職責冗雜的龐大函數,而需要將它們分離成很多小函數。龐大的函數就像黑盒子一樣,很難理解和修改,尤其在測試時更加捉襟見肘。
想象一個場景:一個函數需要返回一個數組、map 或者普通對象的“重量”?!爸亓俊庇蓪傩灾涤嬎愕玫?。規則如下:
舉個例子:數組 [null, 'Hello World', {}] 的重量計算為: 1(null) + 2(字符串類型) + 4(對象) = 7
讓我們從壞的情況開始,所有的邏輯都寫在一個龐大的 getCollectionWeight() 函數里。
在 repl.it 中嘗試運行
function getCollectionWeight(collection) { let collectionValues; if (collection instanceof Array) { collectionValues = collection; } else if (collection instanceof Map) { collectionValues = [...collection.values()]; } else { collectionValues = Object.keys(collection).map(function (key) { return collection[key]; }); } return collectionValues.reduce(function(sum, item) { if (item == null) { return sum + 1; } if (typeof item === 'object' || typeof item === 'function') { return sum + 4; } return sum + 2; }, 0); } let myArray = [null, { }, 15]; let myMap = new Map([ ['functionKey', function() {}] ]); let myObject = { 'stringKey': 'Hello world' }; getCollectionWeight(myArray); // => 7 (1 + 4 + 2) getCollectionWeight(myMap); // => 4 getCollectionWeight(myObject); // => 2
問題顯而易見。getCollectionWeight() 函數過于龐大,看起來像個裝有很多驚喜的黑盒子。你很難眼理解它是做什么的,再想象一下你的應用里有一堆這樣的函數是什么光景。
當你在和這樣的代碼打交道時,是在浪費時間和精力。另一方面小而能夠自解釋的函數讀起來也會讓人愉悅,方便開展之后的工作。
現在我們的目標是把龐大的函數分解成更小的不耦合且可重用的函數。步是通過不同的類型,抽象出決定“重量”值的代碼。這個新函數是 getWeight()。
僅僅看到1、2 和 4 這三個魔數而不了解上下文的情況下根本搞不清楚他們的含義。幸運的是 ES2015 允許我們利用 const 來定義只讀的的變量,所以可以創建有含義的常量來取代魔數。
讓我們創建 getWeightByType() 函數并且改善一下 getCollectionWeight() 函數:
在 repl.it 中嘗試
function getWeightByType(value) { const WEIGHT_NULL_UNDEFINED = 1; const WEIGHT_PRIMITIVE = 2; const WEIGHT_OBJECT_FUNCTION = 4; if (value == null) { return WEIGHT_NULL_UNDEFINED; } if (typeof value === 'object' || typeof value === 'function') { return WEIGHT_OBJECT_FUNCTION; } return WEIGHT_PRIMITIVE; } function getCollectionWeight(collection) { let collectionValues; if (collection instanceof Array) { collectionValues = collection; } else if (collection instanceof Map) { collectionValues = [...collection.values()]; } else { collectionValues = Object.keys(collection).map(function (key) { return collection[key]; }); } return collectionValues.reduce(function(sum, item) { return sum + getWeightByType(item); }, 0); } let myArray = [null, { }, 15]; let myMap = new Map([ ['functionKey', function() {}] ]); let myObject = { 'stringKey': 'Hello world' }; getCollectionWeight(myArray); // => 7 (1 + 4 + 2) getCollectionWeight(myMap); // => 4 getCollectionWeight(myObject); // => 2
是不是看起來好些了?getWeightByType() 函數是無依賴的,僅僅通過數據類型來決定數據的“重量”。你可以在任何一個函數中復用它。getCollectionWeight() 函數也簡變得練了一些。
WEIGHT_NULL_UNDEFINED, WEIGHT_PRIMITIVE 和 WEIGHT_OBJECT_FUNCTION 從變量名就可以看出“重量”所描述的數據類型,而不需要再猜 1, 2 和 4 代表什么。
上面的改進版仍然有瑕疵。想象一下你想要將“重量”的計算應用在 Set 或者其它定制的數據集合時,由于 getCollectionWeight() 函數包含了收集值的邏輯,它的代碼量會快速增長。
讓我們從代碼中抽象出一些函數,比如獲取 map 類型的數據的函數 getMapValues() 和獲取普通對象類型數據的函數 getPlainObjectValues()。再看看新的改進版:
在 repl.it 中嘗試
function getWeightByType(value) { const WEIGHT_NULL_UNDEFINED = 1; const WEIGHT_PRIMITIVE = 2; const WEIGHT_OBJECT_FUNCTION = 4; if (value == null) { return WEIGHT_NULL_UNDEFINED; } if (typeof value === 'object' || typeof value === 'function') { return WEIGHT_OBJECT_FUNCTION; } return WEIGHT_PRIMITIVE; } function getMapValues(map) { return [...map.values()]; } function getPlainObjectValues(object) { return Object.keys(object).map(function (key) { return object[key]; }); } function getCollectionWeight(collection) { let collectionValues; if (collection instanceof Array) { collectionValues = collection; } else if (collection instanceof Map) { collectionValues = getMapValues(collection); } else { collectionValues = getPlainObjectValues(collection); } return collectionValues.reduce(function(sum, item) { return sum + getWeightByType(item); }, 0); } let myArray = [null, { }, 15]; let myMap = new Map([ ['functionKey', function() {}] ]); let myObject = { 'stringKey': 'Hello world' }; getCollectionWeight(myArray); // => 7 (1 + 4 + 2) getCollectionWeight(myMap); // => 4 getCollectionWeight(myObject); // => 2
現在再讀 getCollectionWeight() 函數,你會很容易的弄清楚它實現的功能,現在的函數看起來像一個有趣的故事。每個函數都很清晰并且直截了當,你不會在思考代碼的含義上浪費時間。簡潔的代碼理應如此。
現在依然有很多可以改進的地方。
你可以創建一個獨立的 getCollectionValues() 函數,包含區分數據集合類型的判斷邏輯:
function getCollectionValues(collection) { if (collection instanceof Array) { return collection; } if (collection instanceof Map) { return getMapValues(collection); } return getPlainObjectValues(collection); }
getCollectionWeight() 函數會變得十分簡單,因為它要做的事情就是從 getCollectionValues() 中獲取集合的值,然后執行累加操作。
你也可以創建一個獨立的 reduce 函數:
function reduceWeightSum(sum, item) { return sum + getWeightByType(item); }
因為理想情況下 getCollectionWeight() 中不應該定義匿名函數。
終我們初的龐大函數被拆分成下面這些函數:
在 repl.it 中嘗試
function getWeightByType(value) { const WEIGHT_NULL_UNDEFINED = 1; const WEIGHT_PRIMITIVE = 2; const WEIGHT_OBJECT_FUNCTION = 4; if (value == null) { return WEIGHT_NULL_UNDEFINED; } if (typeof value === 'object' || typeof value === 'function') { return WEIGHT_OBJECT_FUNCTION; } return WEIGHT_PRIMITIVE; } function getMapValues(map) { return [...map.values()]; } function getPlainObjectValues(object) { return Object.keys(object).map(function (key) { return object[key]; }); } function getCollectionValues(collection) { if (collection instanceof Array) { return collection; } if (collection instanceof Map) { return getMapValues(collection); } return getPlainObjectValues(collection); } function reduceWeightSum(sum, item) { return sum + getWeightByType(item); } function getCollectionWeight(collection) { return getCollectionValues(collection).reduce(reduceWeightSum, 0); } let myArray = [null, { }, 15]; let myMap = new Map([ ['functionKey', function() {}] ]); let myObject = { 'stringKey': 'Hello world' }; getCollectionWeight(myArray); // => 7 (1 + 4 + 2) getCollectionWeight(myMap); // => 4 getCollectionWeight(myObject); // => 2
這就是編寫小而美的函數的藝術。
經過一系列的代碼質量優化,你獲得了一連串的好處:
這些優勢會讓你在復雜的應用中如魚得水。
有條通用的準則:一個函數不應該超過20行,小則優。
你現在可能會問我一個合情合理的問題:“我不想為每一行代碼都創建函數,有沒有一個標準讓我不再繼續拆分函數?”這就是下一章節的主題。
讓我們稍作休息,思考一個問題:軟件應用究是什么?每個應用都是為了完成一系列的需求。作為開發者,需要把這些需求分解為可以正確運行特定任務的小組件(命名空間,類,函數,代碼塊)。一個組件包含了其它更小的組件。如果你想要編寫一個組件,需要通過抽象程度比它低一層級的組件來創建。
換句話講:你需要把一個函數分解為多個步驟,這些步驟的抽象程度需要保持在同一層級或者低一層級。這樣可以在保證函數簡練的同時踐行“做一件事,并且做好”的原則。
為什么分解是必要的?因為簡練的函數含義更加明確,也就意味著易讀和易改。
讓我們看一個例子。假設你想要編寫函數實現只保存數組中的素數,移除非素數。函數通過以下方式執行:
getOnlyPrime([2, 3, 4, 5, 6, 8, 11]); // => [2, 3, 5, 11]
在 getOnlyPrime() 函數中有哪些低一層級的抽象步驟?接下來系統闡述:
使用 isPrime() 函數過濾數組中的數字。
需要在這個層級提供 isPrime() 函數的細節嗎?答案是否定的。因為 getOnlyPrime() 函數會有不同層級的抽象步驟,這個函數會包含許多的職責。
既然腦子里有了基礎的想法,讓我們先完成 getOnlyPrime() 函數的內容:
function getOnlyPrime(numbers) { return numbers.filter(isPrime); } getOnlyPrime([2, 3, 4, 5, 6, 8, 11]); // => [2, 3, 5, 11]
此時 getOnlyPrime() 函數非常簡潔。它包含了一個獨立層級的抽象:數組的 .filter() 方法和 isPrime() 函數。
現在是時候向更低的層級抽象了。
數組方法是 .filter() 直接由 JavaScript 引擎提供的,原樣使用即可。ECMA標準中精確地描述了它的功能。
現在我們來研究 isPrime() 函數的具體實現:
為了實現檢查一個數字 n 是否為素數的功能,需要確認是否從 2 到 Math.sqrt(n) 的任意數字都可以整除 n。
理解了這個算法(效率不高,但簡便起見)后,來完成 isPrime() 函數的代碼:
在 repl.it 中嘗試
function isPrime(number) { if (number === 3 || number === 2) { return true; } if (number === 1) { return false; } for (let divisor = 2; divisor <= Math.sqrt(number); divisor++) { if (number % divisor === 0) { return false; } } return true; } function getOnlyPrime(numbers) { return numbers.filter(isPrime); } getOnlyPrime([2, 3, 4, 5, 6, 8, 11]); // => [2, 3, 5, 11]
getOnlyPrime() 函數小而精煉。它僅僅保留了必需的低一層級的抽象。
如果你遵照讓函數簡練化的原則,復雜函數的可讀性可以大大提升。每一層級的精確抽象和編碼可以防止編寫出一大堆難以維護的代碼。
函數名稱應該簡明扼要,不應過于冗長或者簡短。理想情況下,函數名稱應該在不對代碼刨根問底的情況下清楚反映出函數的功能。
函數名稱應該使用駝峰式命名法,以小寫字母開頭:addItem(), saveToStore() 或者 getFirstName()。
因為函數代表了動作,函數名稱應該至少包含一個動詞。比如:deletePage(), verifyCredentials()。獲取或者設置屬性值時,使用標準的 set 和 get 前綴:getLastName() 或者 setLastName()。
避免編寫含混的函數名,比如 foo(), bar(), a(), fun() 等等。這些名稱沒有意義。
如果函數小而清晰,名稱簡明扼要,代碼就可以像散文一樣閱讀。
當然,上面提供的示例十分簡單。真實的應用中會更加復雜。你可能會抱怨僅僅為了抽象出一個層級而編寫簡練的函數是沉悶乏味的任務。但是如果從項目開始之初就正確實踐的話就不會是一件困難的事。
如果應用已經有很多函數擁有太多職責,你會發現很難理解這些代碼。在很多情況下,不大可能在合理的時間完成重構的工作。但是至少從點滴做起:盡你所能抽象一些東西。
好的解決辦法當然是從一開始就正確的實現應用。不僅要在實現需求上花費時間,同樣應該像我建議的那樣:正確組織你的函數,讓它們小而簡練。
三思而后行。(Measure seven times, cut once)
ES2015 實現了一個很棒的模塊系統,清晰地建議出分割函數是好的實踐。
記住永遠值得投資時間讓代碼變得簡練有組織。在這個過程中,你可能覺得實踐起來很難,可能需要很多練習,也可能回過頭來修改一個函數很多次。但沒有比一團亂麻的代碼更糟的了。
本站文章版權歸原作者及原出處所有 。內容為作者個人觀點, 并不代表本站贊同其觀點和對其真實性負責,本站只提供參考并不構成任何投資及應用建議。本站是一個個人學習交流的平臺,網站上部分文章為轉載,并不用于任何商業目的,我們已經盡可能的對作者和來源進行了通告,但是能力有限或疏忽,造成漏登,請及時聯系我們,我們將根據著作權人的要求,立即更正或者刪除有關內容。本站擁有對此聲明的最終解釋權。