摘要:函數需要小要避免編寫職責冗雜的龐大函數,而需要將它們分離成很多小函數。另一方面小而能夠自解釋的函數讀起來也會讓人愉悅,方便開展之后的工作。最終我們最初的龐大函數被拆分成下面這些函數在中嘗試這就是編寫小而美的函數的藝術。
原文鏈接:https://dmitripavlutin.com/th...
譯者:阿里云-也樹
隨著軟件應用的復雜度不斷上升,為了確保應用穩定且易拓展,代碼質量就變的越來越重要。
不幸的是,包括我在內的幾乎每個開發者在職業生涯中都會面對質量很差的代碼。這些代碼通常有以下特征:
函數冗長,做了太多事情
函數有副作用并且很難理解和調試排錯
含糊的函數/變量命名
代碼脆弱,一個小改動會意外地破壞應用的其它組件
缺乏測試的覆蓋
這些話聽起來非常常見:“我不明白這部分代碼怎么工作的”,“這代碼太爛了”,“這代碼太難改了”等等。
有一次我現在的同事因為在之前的團隊處理過難以維護的Ruby 編寫的 REST API 而辭職,他是接手了之前開發團隊的工作。在修復現有的 bug 時會創造新的 bug,添加新的特性也會創造一系列新的 bug,而客戶也不想以更好的設計去重構應用,因而我的同事做了辭職這個正確的決定。
這樣的場景時有發生,我們能做些什么呢?
需要牢記于心的是:僅僅讓應用可以運行和關注代碼質量是不同的。一方面你需要滿足應用的功能,另一方面你需要花時間確認是否任意的函數沒有包含太多職責、是否所有函數都使用了易理解的變量和函數名并且是否避免了函數的副作用。
函數(包括對象的方法)是讓應用運行的小齒輪。首先你應該專注于它們的結構和編寫,而下面這篇文章闡述了編寫清晰易懂且容易測試的函數的最佳實踐。
函數需要“小”要避免編寫職責冗雜的龐大函數,而需要將它們分離成很多小函數。龐大的函數就像黑盒子一樣,很難理解和修改,尤其在測試時更加捉襟見肘。
想象一個場景:一個函數需要返回一個數組、map 或者普通對象的“重量”。“重量”由屬性值計算得到。規則如下:
null 或者 undefined 計為 1
基礎類型的數據計為 2
對象或者函數類型的數據計為 4
舉個例子:數組 [null, "Hello World", {}] 的重量計算為: 1(null) + 2(字符串類型) + 4(對象) = 7
Step 0: 最初的龐大函數讓我們從最壞的情況開始,所有的邏輯都寫在一個龐大的 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() 函數過于龐大,看起來像個裝有很多驚喜的黑盒子。你很難第一眼理解它是做什么的,再想象一下你的應用里有一堆這樣的函數是什么光景。
當你在和這樣的代碼打交道時,是在浪費時間和精力。另一方面小而能夠自解釋的函數讀起來也會讓人愉悅,方便開展之后的工作。
Step 1: 通過數據類型提取“重量”并且去除魔數現在我們的目標是把龐大的函數分解成更小的不耦合且可重用的函數。第一步是通過不同的類型,抽象出決定“重量”值的代碼。這個新函數是 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 代表什么。
Step 2: 繼續分割函數并且增加拓展性上面的改進版仍然有瑕疵。想象一下你想要將“重量”的計算應用在 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() 函數,你會很容易的弄清楚它實現的功能,現在的函數看起來像一個有趣的故事。每個函數都很清晰并且直截了當,你不會在思考代碼的含義上浪費時間。簡潔的代碼理應如此。
Step 3: 永遠不要停止改進現在依然有很多可以改進的地方。
你可以創建一個獨立的 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
這就是編寫小而美的函數的藝術。
經過一系列的代碼質量優化,你獲得了一連串的好處:
通過自解釋的代碼增加了 getCollectionWeight() 函數的可讀性。
極大地減少了 getCollectionWeight() 函數的代碼量。
避免了在你想要增加其它數據集合類型時,getCollectionWeight() 函數代碼量會過于迅速地增長。
抽象出的函數是獨立可重用的。你的同事可能想要引入你這些實用的函數到另一個項目中,你可以輕易的讓他們做到這一點。
如果某個函數意外報錯,函數的調用棧信息會更加清晰,因為它包含了函數名稱,你立刻就能確定出問題的函數在哪里。
分割開的函數更容易編寫測試和實現更高的測試覆蓋率。相比于測試一個龐大函數的所有場景,更好的辦法是獨立構造測試并且獨立核對每一個函數。
你可以利用 CommonJS 或者 ES2015 模塊標準使代碼模塊化。把函數抽象成獨立的模塊,這樣會讓你的項目文件更輕量和結構化。
這些優勢會讓你在復雜的應用中如魚得水。
有條通用的準則:一個函數不應該超過20行,小則優。
你現在可能會問我一個合情合理的問題:“我不想為每一行代碼都創建函數,有沒有一個標準讓我不再繼續拆分函數?”這就是下一章節的主題。
2. 函數應該是簡練的讓我們稍作休息,思考一個問題:軟件應用究竟是什么?
每個應用都是為了完成一系列的需求。作為開發者,需要把這些需求分解為可以正確運行特定任務的小組件(命名空間,類,函數,代碼塊)。
一個組件包含了其它更小的組件。如果你想要編寫一個組件,需要通過抽象程度比它低一層級的組件來創建。
換句話講:你需要把一個函數分解為多個步驟,這些步驟的抽象程度需要保持在同一層級或者低一層級。這樣可以在保證函數簡練的同時踐行“做一件事,并且做好”的原則。
為什么分解是必要的?因為簡練的函數含義更加明確,也就意味著易讀和易改。
讓我們看一個例子。假設你想要編寫函數實現只保存數組中的素數,移除非素數。函數通過以下方式執行:
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() 函數小而精煉。它僅僅保留了必需的低一層級的抽象。
如果你遵照讓函數簡練化的原則,復雜函數的可讀性可以大大提升。每一層級的精確抽象和編碼可以防止編寫出一大堆難以維護的代碼。
3. 使用簡明扼要的函數名稱函數名稱應該簡明扼要,不應過于冗長或者簡短。理想情況下,函數名稱應該在不對代碼刨根問底的情況下清楚反映出函數的功能。
函數名稱應該使用駝峰式命名法,以小寫字母開頭:addItem(), saveToStore() 或者 getFirstName()。
因為函數代表了動作,函數名稱應該至少包含一個動詞。比如:deletePage(), verifyCredentials()。獲取或者設置屬性值時,使用標準的 set 和 get 前綴:getLastName() 或者 setLastName()。
避免編寫含混的函數名,比如 foo(), bar(), a(), fun() 等等。這些名稱沒有意義。
如果函數小而清晰,名稱簡明扼要,代碼就可以像散文一樣閱讀。
4. 結論當然,上面提供的示例十分簡單。真實的應用中會更加復雜。你可能會抱怨僅僅為了抽象出一個層級而編寫簡練的函數是沉悶乏味的任務。但是如果從項目開始之初就正確實踐的話就不會是一件困難的事。
如果應用已經有很多函數擁有太多職責,你會發現很難理解這些代碼。在很多情況下,不大可能在合理的時間完成重構的工作。但是至少從點滴做起:盡你所能抽象一些東西。
最好的解決辦法當然是從一開始就正確的實現應用。不僅要在實現需求上花費時間,同樣應該像我建議的那樣:正確組織你的函數,讓它們小而簡練。
三思而后行。(Measure seven times, cut once)
ES2015 實現了一個很棒的模塊系統,清晰地建議出分割函數是好的實踐。
記住永遠值得投資時間讓代碼變得簡練有組織。在這個過程中,你可能覺得實踐起來很難,可能需要很多練習,也可能回過頭來修改一個函數很多次。
但沒有比一團亂麻的代碼更糟的了。
5. 譯者注文章作者提出的 small function 的觀點可能會讓初學者產生一點誤解,在我的理解里,更準確的表述應該是從代碼實現功能的邏輯層面抽象出更小的功能點,將抽象出的功能點轉化為函數來為最后的業務提供組裝的零件。最終的目的依然是通過解耦邏輯來提高代碼的拓展性和復用性,而不能僅僅停留在視覺層面的”小“,單純為了讓函數代碼行數變少是沒有意義的。
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/92804.html
摘要:項目地址移動端微型插件,小而美,無依賴前言相對于其他的插件而言,最大的優勢就是小,壓縮后僅能滿足部分開發需求。插件的開發采用組合使用構造函數模式和原型模式,通過構建,感興趣的可以閱讀源碼。 mSwiper.js showImg(https://segmentfault.com/img/remote/1460000011106820); showImg(https://segmen...
摘要:項目地址移動端微型插件,小而美,無依賴前言相對于其他的插件而言,最大的優勢就是小,壓縮后僅能滿足部分開發需求。插件的開發采用組合使用構造函數模式和原型模式,通過構建,感興趣的可以閱讀源碼。 mSwiper.js showImg(https://segmentfault.com/img/remote/1460000011106820); showImg(https://segmen...
摘要:項目地址移動端微型插件,小而美,無依賴前言相對于其他的插件而言,最大的優勢就是小,壓縮后僅能滿足部分開發需求。插件的開發采用組合使用構造函數模式和原型模式,通過構建,感興趣的可以閱讀源碼。 mSwiper.js showImg(https://segmentfault.com/img/remote/1460000011106820); showImg(https://segmen...
摘要:引言日常工作中再牛逼的大佬都不敢說自己的代碼是完全沒有問題的,既然有問題,那就也就有調試,說到調試工具,大家可能對于還有遠程調試等比較熟悉,甚至有些是我可能也沒有用過的這里噴一句吧,誰都別給我提啊,那個不叫調試工具,那叫坑爹神器,話說最近不 showImg(https://segmentfault.com/img/bVbk8zn?w=1008&h=298); 引言 ? 日常工作中再牛...
摘要:還有一些小功能,比如滑動邊界遞減,自動滑動,垂直水平滑動可配置后續我們計劃增加手勢縮放圖片頁面內部切換等更強大的功能,希望有更多的人來使用,也歡迎大家提交和爭取打造最好用的移動端滑動組件。 iSlider是一個專為移動端設計的滑動組件,項目地址: https://github.com/BE-FE/iSlider iSlider是我參與的第二個比較正式的開源項目,主要編寫了里面的動畫部...
閱讀 3475·2021-10-13 09:39
閱讀 1458·2021-10-08 10:05
閱讀 2260·2021-09-26 09:56
閱讀 2275·2021-09-03 10:28
閱讀 2673·2019-08-29 18:37
閱讀 2032·2019-08-29 17:07
閱讀 600·2019-08-29 16:23
閱讀 2191·2019-08-29 11:24