原文連接: https://dmitripavlutin.com/th...
譯者:阿里雲-也樹
隨着軟件應用的複雜度不斷上升,爲了確保應用穩定且易拓展,代碼質量就變的愈來愈重要。算法
不幸的是,包括我在內的幾乎每一個開發者在職業生涯中都會面對質量不好的代碼。這些代碼一般有如下特徵:數組
這些話聽起來很是常見:「我不明白這部分代碼怎麼工做的」,「這代碼太爛了」,「這代碼太難改了」等等。模塊化
有一次我如今的同事由於在以前的團隊處理過難以維護的Ruby 編寫的 REST API 而辭職,他是接手了以前開發團隊的工做。在修復現有的 bug 時會創造新的 bug,添加新的特性也會創造一系列新的 bug,而客戶也不想以更好的設計去重構應用,於是個人同事作了辭職這個正確的決定。函數
這樣的場景時有發生,咱們能作些什麼呢?測試
須要牢記於心的是:僅僅讓應用能夠運行和關注代碼質量是不一樣的。一方面你須要知足應用的功能,另外一方面你須要花時間確認是否任意的函數沒有包含太多職責、是否全部函數都使用了易理解的變量和函數名而且是否避免了函數的反作用。優化
函數(包括對象的方法)是讓應用運行的小齒輪。首先你應該專一於它們的結構和編寫,而下面這篇文章闡述了編寫清晰易懂且容易測試的函數的最佳實踐。阿里雲
要避免編寫職責冗雜的龐大函數,而須要將它們分離成不少小函數。龐大的函數就像黑盒子同樣,很難理解和修改,尤爲在測試時更加捉襟見肘。編碼
想象一個場景:一個函數須要返回一個數組、map 或者普通對象的「重量」。「重量」由屬性值計算獲得。規則以下:spa
null
或者 undefined
計爲 1
2
4
舉個例子:數組 [null, 'Hello World', {}]
的重量計算爲: 1
(null
) + 2
(字符串類型) + 4
(對象) = 7
prototype
讓咱們從最壞的狀況開始,全部的邏輯都寫在一個龐大的 getCollectionWeight()
函數裏。
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()
函數:
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()
。再看看新的改進版:
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()
中不該該定義匿名函數。
最終咱們最初的龐大函數被拆分紅下面這些函數:
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
這就是編寫小而美的函數的藝術。
通過一系列的代碼質量優化,你得到了一連串的好處:
getCollectionWeight()
函數的可讀性。getCollectionWeight()
函數的代碼量。getCollectionWeight()
函數代碼量會過於迅速地增加。這些優點會讓你在複雜的應用中如魚得水。
有條通用的準則:一個函數不該該超過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()
函數的代碼:
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 實現了一個很棒的模塊系統,清晰地建議出分割函數是好的實踐。
記住永遠值得投資時間讓代碼變得簡練有組織。在這個過程當中,你可能以爲實踐起來很難,可能須要不少練習,也可能回過頭來修改一個函數不少次。
但沒有比一團亂麻的代碼更糟的了。
文章做者提出的 small function
的觀點可能會讓初學者產生一點誤解,在個人理解裏,更準確的表述應該是從代碼實現功能的邏輯層面抽象出更小的功能點,將抽象出的功能點轉化爲函數來爲最後的業務提供組裝的零件。最終的目的依然是經過解耦邏輯來提升代碼的拓展性和複用性,而不能僅僅停留在視覺層面的」小「,單純爲了讓函數代碼行數變少是沒有意義的。