代碼的藝術:如何寫出小而清晰的函數

hijacked 8年前發布 | 7K 次閱讀 JavaScript開發 JavaScript

本文以 JavaScript 為例,介紹了該如何優化函數,使函數清晰易讀,且更加高效穩定。

軟件的復雜度一直在持續增長。代碼質量對于保證應用的可靠性、易擴展性非常重要。

然而,幾乎每一個開發者,包括我自己,在職業生涯中都見過低質量的代碼。這東西就是個坑。低質量代碼具備以下極具殺傷力的特點:

  • 函數超級長,而且塞滿了各種亂七八糟的功能。

  • 函數通常有一些副作用,不僅難以理解,甚至根本沒法調試。

  • 含糊的函數、變量命名。

  • 脆弱的代碼:一個小的變更,就有可能出乎意料的破壞其他應用組件。

  • 代碼覆蓋率缺失。

它們聽起來基本都是: “我根本沒法理解這段代碼是如何工作的”,“這段代碼就是一堆亂麻”,“要修改這一段代碼實在太難了” 等等。

我就曾遇到過這樣的情況,我的一個同事由于無法繼續將一個基于Ruby 的 REST API 做下去,繼而離職。這個項目是他從之前的開發團隊接手的。

修復現有的 bug ,然后引入了新的 bug,添加新的特性,就增加了一連串新 bug,如此循環(所謂的脆弱代碼)。客戶不希望以更好的設計重構整個應用,開發人員也做出明智的選擇——維持現狀。

好吧,這種事兒經常發生,而且挺糟糕的。那我們能做點什么呢?

首先,需要謹記于心:只是讓應用運轉起來,和盡心保證代碼質量是兩個完全不同的事。一方面,你需要實現產品需求。但是另一方面,你應該花點時間,確保函數功能簡單、使用易讀的變量和函數命名,避免函數的副作用等等。

函數(包括對象方法)是讓應用運轉起來的齒輪。首先你應當將注意力集中在他們的結構和整體布局上。這篇文章包括了一些非常好的示例,展示如何編寫清晰、易于理解和測試的函數。

1. 函數應當很小,非常小

避免使用包含大量的功能的大函數,應當將其功能分割為若干較小的函數。大的黑盒函數難于理解、修改,特別是很難測試。

假設這樣一個場景,需要實現一個函數,用于計算 array、map 或 普通 JavaScript 對象的權重。總權重可通過計算各成員權重獲得:

  • null 或者 未定義變量計 1 點。

  • 基本類型計 2 點。

  • 對象或函數計 4 點。

例如,數組 [null, ‘Hello World’, {}] 的權重這樣計算:1(null) + 2(string 是基本類型) + 4(對象) = 7。

Step 0: 最初的大函數

我們從最糟的實例開始。所有的邏輯都被編碼在函數 getCollectionWeight() 中:

 

function getCollectionWeight(collection) {  
  letcollectionValues;
  if (collectioninstanceof Array) {
    collectionValues = collection;
  } else if (collectioninstanceof 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);
}
letmyArray = [null, { }, 15];  
letmyMap = new Map([ ['functionKey', function() {}] ]);  
letmyObject = { 'stringKey': 'Hello world' };  
getCollectionWeight(myArray);  // => 7 (1 + 4 + 2)  
getCollectionWeight(myMap);    // => 4  
getCollectionWeight(myObject); // => 2

問題顯而易見,getCollectionWeight() 函數超級長,而且看起來像一個裝滿“意外”的黑盒子。可能你也發現了,第一眼根本就搞不明白它要干什么。再試想一下,應用里有大把這樣的函數。

在工作中遇到這樣的代碼,就是在浪費你的時間和精力。反之,高質量的代碼不會令人不適。高質量代碼中,那些精巧、自文檔極好的函數非常易于閱讀和理解。

Step 1:根據類型計算權重,拋棄那些“迷之數字”。

現在,我們的目標是:把這個巨型函數,拆分為較小的、獨立的、可重用的一組函數。第一步,將根據類型計算權重的代碼提取出來。這個新的函數命名為 getWeight()。

我們再看看這幾個“迷之數字”: 1, 2, 4。在不知道整個故事背景的前提下,僅靠這幾個數字提供不了任何有用的信息。幸好 ES2015 允許定義靜態只讀引用,那你就能簡單的創造幾個常量,用有意義的名稱,替換掉那幾個“迷之數字”。(我特別喜歡“迷之數字”這個說法:D)

我們來新建一個較小的函數 getWeightByType(),并用它來改進 getCollectionWeight():

 

// Code extracted into getWeightByType()
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) {  
  letcollectionValues;
  if (collectioninstanceof Array) {
    collectionValues = collection;
  } else if (collectioninstanceof 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);
}
letmyArray = [null, { }, 15];  
letmyMap = new Map([ ['functionKey', function() {}] ]);  
letmyObject = { '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() 會快速膨脹,因為它包含了一組獲得權值的具體邏輯。

讓我們將獲得 maps 權重的代碼提取到 getMapValues() ,將獲得基本 JavaScript 對象權值的代碼則放到 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;
}
// Code extracted into getMapValues()
function getMapValues(map) {  
  return [...map.values()];
}
// Code extracted into getPlainObjectValues()
function getPlainObjectValues(object) {  
  return Object.keys(object).map(function (key) {
    return object[key];
  });
}
function getCollectionWeight(collection) {  
  letcollectionValues;
  if (collectioninstanceof Array) {
    collectionValues = collection;
  } else if (collectioninstanceof Map) {
    collectionValues = getMapValues(collection);
  } else {
    collectionValues = getPlainObjectValues(collection);
  }
  return collectionValues.reduce(function(sum, item) {
    return sum + getWeightByType(item);
  }, 0);
}
letmyArray = [null, { }, 15];  
letmyMap = new Map([ ['functionKey', function() {}] ]);  
letmyObject = { 'stringKey': 'Hello world' };  
getCollectionWeight(myArray);  // => 7 (1 + 4 + 2)  
getCollectionWeight(myMap);    // => 4  
getCollectionWeight(myObject); // => 2

現在再來看 getCollectionWeight() 函數,你會發現已經比較容易明白它的機理,看起來就像一段有趣的故事。

每一個函數的簡單明了。你不需要花費時間去挖掘代碼,理解代碼的工作。這就是清新版代碼該有的樣子。

Step 3: 優化永無止境

就算到了現在這種程度,依然有很大優化的空間!

你可以創建一個獨立的函數 getCollectionValues() ,使用 if/else 語句區分集合中的類型:

function getCollectionValues(collection) {  
  if (collectioninstanceof Array) {
    return collection;
  }
  if (collectioninstanceof Map) {
    return getMapValues(collection);
  }
  return getPlainObjectValues(collection);
}

那么, getCollectionWeight() 應該會變得異常純粹,因為它唯一的工作:用 getCollectionValues() 獲得集合中的值,然后依次調用求和累加器。

你也可以創建一個獨立的累加器函數:

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 (collectioninstanceof Array) {
    return collection;
  }
  if (collectioninstanceof Map) {
    return getMapValues(collection);
  }
  return getPlainObjectValues(collection);
}
function reduceWeightSum(sum, item) {  
  return sum + getWeightByType(item);
}
function getCollectionWeight(collection) {  
  return getCollectionValues(collection).reduce(reduceWeightSum, 0);
}
letmyArray = [null, { }, 15];  
letmyMap = new Map([ ['functionKey', function() {}] ]);  
letmyObject = { 'stringKey': 'Hello world' };  
getCollectionWeight(myArray);  // => 7 (1 + 4 + 2)  
getCollectionWeight(myMap);    // => 4  
getCollectionWeight(myObject); // => 2

這就是編寫簡單精美函數的藝術!

除了這些代碼質量上的優化之外,你也得到不少其他的好處:

  • 通過代碼自文檔,getCollectionWeight() 函數的可讀性得到很大提升。

  • getCollectionWeight() 函數的長度大幅減少。

  • 如果你打算計算其他類型的權重值,getCollectionWeight() 的代碼不會再劇烈膨脹了。

  • 這些拆分出來的函數都是低耦合、高可復用的組件,你的同事可能希望將他們導入其他項目中,而你可以輕而易舉的實現這個要求。

  • 當函數偶發錯誤的時候,調用棧會更加詳細,因為棧中包含函數的名稱,甚至你可以立馬發現出錯的函數。

  • 這些小函數更簡單、易測試,可以達到很高的代碼覆蓋率。與其窮盡各種場景來測試一個大函數,你可以進行結構化測試,分別測試每一個小函數。

  • 你可以參照 CommonJS 或 ES2015 模塊格式,將拆分出的函數創建為獨立的模塊。這將使得你的項目文件更輕、更結構化。

這些建議可以幫助你,戰勝應用的復雜性。

原則上,你的函數不應當超過 20 行——越小越好。

現在,我覺得你可能會問我這樣的問題:“我可不想將每一行代碼都寫為函數。有沒有什么準則,告訴我何時應當停止拆分?”。這就是接下來的議題了。

2. 函數應當是簡單的

讓我們稍微放松一下,思考下應用的定義到底是什么?

每一個應用都需要實現一系列需求。開發人員的準則在于,將這些需求拆分為一些列較小的可執行組件(命名空間、類、函數、代碼塊等),分別完成指定的工作。

一個組件又由其他更小的組件構成。如果你希望編寫一個組件,你只能從抽象層中低一級的組件中,選取需要的組件用于創建自己的組件。

換言之,你需要將一個函數分解為若干較小的步驟,并且保證這些步驟都在抽象上,處于同一級別,而且只向下抽象一級。這非常重要,因為這將使得函數變得簡單,做到“做且只做好一件事”。

為什么這是必要的?因為簡單的函數非常清晰。清晰就意味著易于理解和修改。

我們來舉個例子。假設你需要實現一個函數,使數組僅保留 素數 (2, 3, 5, 7, 11 等等),移除非素數(1, 4, 6, 8 等等)。函數的調用方式如下:

getOnlyPrime([2, 3, 4, 5, 6, 8, 11]); // => [2, 3, 5, 11]

如何用低一級抽象的若干步驟實現 getOnlyPrime() 函數呢?我們這樣做:

為了實現 getOnlyPrime() 函數, 我們用 isPrime() 函數來過濾數組中的數字。

非常簡單,只需要對數字數組執行一個過濾函數 isPrime() 即可。

你需要在當前抽象層實現 isPrime() 的細節嗎?不,因為 getOnlyPrime() 函數會在不同的抽象層實現一些列步驟。否則,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 引擎提供,我們直接使用即可。當然,標準已經 準確描述 了它的行為。

現在你可以深入如何實現 isPrime() 的細節中了:

為了實現 isPrime() 函數檢查一個數字 n 是否為素數,只需要檢查 2 到 Math.sqrt(n) 之間的所有整數是否均不能整除n。

有了這個算法(不算高效,但是為了簡單起見,就用這個吧),我們來為 isPrime() 函數編碼:

 

function isPrime(number) {  
  if (number === 3 || number === 2) {
    return true;
  }
  if (number === 1) {
    return false;
  }
  for (letdivisor = 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() 。需要 get 或 set 屬性的時候,請使用 標準的  set 和 get 前綴: getLastName() 或  setLastName() 。

避免在生產代碼中出現有誤導性的名稱,例如 foo(), bar(), a(), fun() 等等。這樣的名稱沒有任何意義。

如果函數都短小清晰,命名簡練:代碼讀起來就會像詩一樣迷人。

4. 總結

當然了,這里假定的例子都非常簡單。現實中的代碼更加復雜。你可能要抱怨,編寫清晰的函數,只在抽象上一級一級下降,實在太沒勁了。但是如果從項目一開始就開始你的實踐,就遠沒有想象中復雜。

如果應用中已經存在一些功能繁雜的函數,希望對它們進行重構,你可能會發現困難重重。而且在很多情況下,在合理的時間內是不可能完成的。但千里之行始于足下:在力所能及的前提下,先拆分一部分出來。

當然,最正確的解決方案應該是,從項目一開始就以正確的方式實現應用。除了花一些時間在實現上,也應該花一些精力在組建合理的函數結構上:如我們所建議的——讓它們保持短小、清晰。

ES2015 實現了一個非常棒的模塊系統,它明確建議,小函數是優秀的工程實踐。

記住,干凈、組織良好的代碼通常 需要投入大量時間 。你會發現這做起來有難度。可能需要很多嘗試,可能會迭代、修改一個函數很多次。

然而,沒有什么比亂麻一樣的代碼更讓人痛心的了,那么這一切都是值得的!

 

來自:http://blog.jobbole.com/106720/

 

 本文由用戶 hijacked 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!