編寫小而美函數的藝術

原文連接: https://dmitripavlutin.com/th...
譯者:阿里雲-也樹

隨着軟件應用的複雜度不斷上升,爲了確保應用穩定且易拓展,代碼質量就變的愈來愈重要。算法

不幸的是,包括我在內的幾乎每一個開發者在職業生涯中都會面對質量不好的代碼。這些代碼一般有如下特徵:數組

  • 函數冗長,作了太多事情
  • 函數有反作用而且很難理解和調試排錯
  • 含糊的函數/變量命名
  • 代碼脆弱,一個小改動會意外地破壞應用的其它組件
  • 缺少測試的覆蓋

這些話聽起來很是常見:「我不明白這部分代碼怎麼工做的」,「這代碼太爛了」,「這代碼太難改了」等等。模塊化

有一次我如今的同事由於在以前的團隊處理過難以維護的Ruby 編寫的 REST API 而辭職,他是接手了以前開發團隊的工做。在修復現有的 bug 時會創造新的 bug,添加新的特性也會創造一系列新的 bug,而客戶也不想以更好的設計去重構應用,於是個人同事作了辭職這個正確的決定。函數

圖片描述

這樣的場景時有發生,咱們能作些什麼呢?測試

須要牢記於心的是:僅僅讓應用能夠運行和關注代碼質量是不一樣的。一方面你須要知足應用的功能,另外一方面你須要花時間確認是否任意的函數沒有包含太多職責、是否全部函數都使用了易理解的變量和函數名而且是否避免了函數的反作用。優化

函數(包括對象的方法)是讓應用運行的小齒輪。首先你應該專一於它們的結構和編寫,而下面這篇文章闡述了編寫清晰易懂且容易測試的函數的最佳實踐。阿里雲

函數須要「小」

要避免編寫職責冗雜的龐大函數,而須要將它們分離成不少小函數。龐大的函數就像黑盒子同樣,很難理解和修改,尤爲在測試時更加捉襟見肘。編碼

想象一個場景:一個函數須要返回一個數組、map 或者普通對象的「重量」。「重量」由屬性值計算獲得。規則以下:spa

  • null 或者 undefined 計爲 1
  • 基礎類型的數據計爲 2
  • 對象或者函數類型的數據計爲 4

舉個例子:數組 [null, 'Hello World', {}] 的重量計算爲: 1(null) + 2(字符串類型) + 4(對象) = 7prototype

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()

僅僅看到124 這三個魔數而不瞭解上下文的狀況下根本搞不清楚他們的含義。幸運的是 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_PRIMITIVEWEIGHT_OBJECT_FUNCTION 從變量名就能夠看出「重量」所描述的數據類型,而不須要再猜 1, 24 表明什麼。

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 是否爲素數的功能,須要確認是否從 2Math.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()。獲取或者設置屬性值時,使用標準的 setget 前綴:getLastName() 或者 setLastName()

避免編寫含混的函數名,好比 foo(), bar(), a(), fun() 等等。這些名稱沒有意義。

若是函數小而清晰,名稱簡明扼要,代碼就能夠像散文同樣閱讀。

4. 結論

固然,上面提供的示例十分簡單。真實的應用中會更加複雜。你可能會抱怨僅僅爲了抽象出一個層級而編寫簡練的函數是沉悶乏味的任務。可是若是從項目開始之初就正確實踐的話就不會是一件困難的事。

若是應用已經有不少函數擁有太多職責,你會發現很難理解這些代碼。在不少狀況下,不大可能在合理的時間完成重構的工做。可是至少從點滴作起:盡你所能抽象一些東西。

最好的解決辦法固然是從一開始就正確的實現應用。不只要在實現需求上花費時間,一樣應該像我建議的那樣:正確組織你的函數,讓它們小而簡練。

三思然後行。(Measure seven times, cut once)

圖片描述

ES2015 實現了一個很棒的模塊系統,清晰地建議出分割函數是好的實踐。

記住永遠值得投資時間讓代碼變得簡練有組織。在這個過程當中,你可能以爲實踐起來很難,可能須要不少練習,也可能回過頭來修改一個函數不少次。

但沒有比一團亂麻的代碼更糟的了。

5. 譯者注

文章做者提出的 small function 的觀點可能會讓初學者產生一點誤解,在個人理解裏,更準確的表述應該是從代碼實現功能的邏輯層面抽象出更小的功能點,將抽象出的功能點轉化爲函數來爲最後的業務提供組裝的零件。最終的目的依然是經過解耦邏輯來提升代碼的拓展性和複用性,而不能僅僅停留在視覺層面的」小「,單純爲了讓函數代碼行數變少是沒有意義的。

相關文章
相關標籤/搜索