提升代碼質量:如何編寫函數

函數是實現程序功能的最基本單位,每個程序都是由一個個最基本的函數構成的。寫好一個函數是提升程序代碼質量最關鍵的一步。本文就函數的編寫,從函數命名,代碼分佈,技巧等方面入手,談談如何寫好一個可讀性高、易維護,易測試的函數。ajax

命名

首先從命名提及,命名是提升可讀性的第一步。如何爲變量和函數命名一直是開發者心中的痛點之一,對於母語非英語的咱們來講,更是難上加難。下面我來講說如何爲函數命名的一些想法和感覺:數組

採用統一的命名規則

在談及如何爲函數取一個準確而優雅的名字以前,首先最重要的是要有統一的命名規則。這是提升代碼可讀性的最基礎的準則。
帕斯卡命名法和駝峯命名法是目前比較流行的兩種規則,不一樣語言採用的規則可能不同,可是要記住一點:保持團隊和我的風格一致。
一、帕斯卡命名法
帕斯卡命名法簡單地說就是:多個單詞組成一個名稱時,每一個單詞的首字母大寫。好比:ide

1 public void SendMessage ();
2 public void CalculatePrice ();

在C#中,這種命名法經常使用於類、屬性,函數等等,在JS中,構造函數也推薦採用這種方式命名。函數

二、駝峯命名法
駝峯命名法和帕斯卡命名法很相似,多個單詞組成一個名稱時,第一個單詞所有小寫,後面單詞首字母大寫。好比:post

1 var sendMessage = function () {};
2 var calculatePrice = function () {};

駝峯命名法通常用於字段、局部變量、函數參數等等。,在JS中,函數也經常使用此方法命名。單元測試

採用哪一種命名規則並不絕對,最重要的是要遵照團隊約定,語言規範。測試

儘量完整地描述函數所作的全部事情

有的開發者可能以爲相較於長函數名來講,短函數名看起來可能更簡潔,看起來也更舒服。可是一般來講,函數名稱越短其描述的意思越抽象。函數使用者對函數的第一印象就是函數名稱,進而瞭解函數的功能,咱們應該儘量地描述到函數所作的全部事情,防止使用者不知道或誤解形成潛在的錯誤。
舉個例子,假設咱們作一個添加評論的功能,添加完畢後並返回評論總數量,如何命名比較合適呢?fetch

1 // 描述不夠完整的函數名
2 var count = function addComment() {};
3 
4 // 描述完整的函數名
5 var count = function addCommentAndReturnCount() {};

這只是簡單的一個例子,實際開發中可能會遇到得更多複雜的狀況,單一職責原則是咱們開發函數要遵照的準則,可是有時候沒法作到函數單一職責時,請記得函數名應該儘量地描述全部事情。當你沒法命名一個函數時,應該分析一下,這個函數的編寫是否科學,有什麼辦法能夠去優化它。優化

採用準確的描述動詞

這一點對母語非英語的開發者來講應該是比較難的一點,想要提升這方面的能力,最主要的仍是要提升詞彙量,多閱讀優秀代碼積累經驗。
這裏簡單說說我本身的一些感想和見解:
一、不要採用太抽象普遍的單詞
不少開發人員會採用一個比較寬泛的動詞來爲函數命名,最典型的一個例子就是get這個單詞。咱們平時開發中常常會經過各類不一樣的方式拿到數據,可是每一種方式都用get就有點太抽象了。具體如何命名,要具體分析:
(1)簡單的返回數據ui

1 Person.prototype.getFullName = function() {
2     return this.firstName = this.lastName;
3 }

(2)從遠程獲取數據

1 var fetchPersons = function () {
2     ...
3     $.ajax({
4     })
5 }

(3)從本地存儲加載數據

1 var loadPersons = function () {};

(4)經過計算獲取數據

1 var calculateTotal = function () {};

(5)從數組中查找數據

1 var findSth = function (arr) {};

(6)從一些數據生成或獲得

1 var createSth = function (data) {};
2 var buildSth = function (data) {};
3 var parseSth = function(data) {};

這是一個簡單的例子,咱們平時開發中遇到的狀況確定會複雜得多,關鍵仍是靠單詞的積累,多閱讀優秀源碼

下面是整理的一些經常使用的對仗詞,你們能夠參考使用

1 add/remove        increment/decrement       open/close
2 begin/end            insert/delete                      show/hide
3 create/destory    lock/unlock                        source/target
4 first/last              min/max                             star/stop
5 get/put                next/previous                     up/down     
6 get/set                old/new

根據不一樣項目和需求制定好命名規則

這一點也是很重要的,尤爲是在團隊合做中,不一樣的項目和需求可能致使的不一樣的命名規則。
好比咱們一般採用的命名規則是動賓結構,也就是動詞在前,名詞災後。可是有一些項目,好比數據接口等項目中,有的團隊會採用名字在前,動詞在後的形式,例如:

1 public static Product[] ProductsGet(){};
2 public static Product[] ProductsDel(){};
3 public static Customer[] CustomerDel(){};
4 public static Customer[] CustomerDel(){};

這種的好處是看到前面的名詞,好比ProductsGet,就能很快的知道這是產品相關的數據接口。
固然這個並非絕對的,關鍵仍是要團隊共同制定和遵照同一套命名規則。

函數參數

函數使用者在調用函數時,必須嚴格遵照函數定義的參數,這對函數的易用性,可測試性等方面都是相當重要的。下面我從幾個方面來談談關於如何優化好函數參數的一些想法。

參數數量

毫無疑問,函數參數越多,函數的易用性就越差,由於使用者須要嚴格眼中參數列表依次輸入參數,若是某個參數輸錯,將致使不可意料的結果。
可是,函數參數就必定越少越好嗎?咱們來看看下面的例子:

1 var count = 0;
2 var unitPrice = 1.5;
3 ....
4 ...
5 var calculatePrice = function () {
6     return count * unitPrice;
7 }

在這個例子中,咱們經過calculatePrice這個函數來計算價格,函數不接收任何參數,直接經過兩個全局變量unitPrice和count進行計算。這種函數的定義對使用者來講很是方便,直接調用便可,不用輸入任何參數。可是這裏可能會有潛在的bug:全局變量可能在其餘地方被修改爲其餘值了,難以進行單元測試等等問題。因此,這個函數能夠傳入數量和價格信息:

1 var calculatePrice = function(count, unitPrice) {
2     return count * unitPrice;
3 }

這種方式下,函數使用者在使用時,要傳入參數進行調用,避免了全局變量可能存在的問題。另外也下降了耦合,提升了可測試性,在測試的時候就沒必要依賴於全局變量。

固然,在保證函數不依賴於全局變量和測試性的狀況下,函數參數仍是越少越好。《代碼大全》中提出將函數的參數限制在7個之內,這個能夠做爲咱們的參考。
有的時候,咱們不可避免地要使用超過10個以上函數,在這中狀況下,咱們能夠考慮將相似的參數構形成一個類,咱們來看看一個典型的例子。
我相信你們平時必定作過這樣的功能,列表篩選,其中涉及到各類條件的篩選,排序,分頁等等功能,若是將參數一個一個地列出來一定會很長,例如:

1 var filterHotel = function (city, checkIn, checkOut, price, star, position, wifi, meal, sort, pageIndex) {}

這是一個篩選酒店的函數,其中的參數分別是城市,入住和退房時間,價格,星級,位置,是否有wifi,是否有早餐,排序,頁碼等等,實際的狀況可能會更多。在這種參數特別多的狀況下,咱們能夠考慮將一些類似的參數提取成類出來:

 1 function DatePlace (city, checkIn, checkOut){
 2     this.city = city;
 3     this.checkIn = checkIn;
 4     this.checkOut = checkOut
 5 }
 6 
 7 function HotelFeature (price, star, position, wifi, meal){
 8     this.price = price;
 9     this.star = star;
10     this.position = position;
11     this.wifi = wifi;
12     this.meal = meal;
13 }
14 
15 var filterHotel = function (datePlce, hotelFeature, sort, pageIndex) {};

將多個參數提取成對象了,雖然對象數量增多了,可是函數參數更清晰了,調用起來也更方便了。

儘可能不要使用bool類型做爲參數

有的時候,咱們會寫出使用bool做爲參數的狀況,好比:

1 var getProduct = function(finished) {
2     if(finished){
3     }
4     else{
5     }
6 }
7 
8 // 調用
9 getProduct(true);

若是沒有註釋,使用者看到這樣的代碼:getProduct(true),他確定搞不清楚true是表明什麼意思,還要去查看函數定義才能明白這個函數是如何使用的。這就意味着這個函數不夠清晰,就應該考慮去優化它。一般有兩種方式去優化它:
(1)將函數一分爲二,分紅兩個函數getFinishedProduct和getUnFinishedProduct
(2)將bool轉換成有意義的枚舉getProduct(ProductStatus)

不要修改輸入參數

若是輸入參數在函數內被修改了,頗有可能形成潛在的bug,並且使用者不知道調用函數後竟然會修改函數參數。
正確使用輸入參數的作法應該是隻傳入參數用於函數調用。
若是不可避免地要修改,必定要在註釋中說明。

儘可能不要使用輸出參數

使用輸出參數說明這個函數不僅作了一件事情,並且使用者使用的時候可能還會感到困惑。正確的方式應該是分解函數,讓函數只作一件事。

編寫函數體

函數體就是實現函數功能的整個邏輯,是一個函數最關鍵的地方。下面我談談關於函數代碼編寫的一些我的想法。

相關操做放在一塊兒

有的時候,咱們會在一個函數內進行一系列的操做來完成一個功能,好比:

1 var calculateTotalPrice = function()  {
2     var roomCount = getRoomCount();
3     var mealCount = getMealCount();
4 
5     var roomPrice = getRoomPrice(roomCount);
6     var mealPrice = getMealPrice(mealCount);
7 
8     return roomPrice + mealPrice;
9 }

這段代碼計算了房間價格和早餐價格,而後將二者相加返回總價格。
這段代碼乍一看,沒有什麼問題,可是咱們分析代碼,咱們先是分別獲取了房間數量和早餐數量,而後再經過房間數量和早餐數量分別計算二者的價格。這種狀況下,房間數量和計算房間價格的代碼分散在了兩個位置,早餐價格的計算也是分散到了兩個位置。也就是兩部分相關的代碼分散在了各處,這樣閱讀起代碼來邏輯會略顯不通,代碼組織不夠好。咱們應該讓相關的語句和操做放在一塊兒,也有利於重構代碼。咱們修改以下:

1 var calculateTotalPrice = function()  {
2     var roomCount = getRoomCount();
3     var roomPrice = getRoomPrice(roomCount);
4 
5     var mealCount = getMealCount();
6     var mealPrice = getMealPrice(mealCount);
7 
8     return roomPrice + mealPrice;
9 }

咱們將相關的操做放在一塊兒,這樣代碼看起來更清晰了,並且也更容易重構了。

儘可能減小代碼嵌套

咱們平時寫if,switch或for語句是常有的事兒,也必定寫過多層if或for語句嵌套的狀況,若是代碼裏的嵌套超過3層,閱讀起來就會很是困難了。咱們應該儘可能避免代碼嵌套多層,最好不要超過2層。下面我來講說我平時一些減小嵌套的技巧或方法。

if語句嵌套的問題

多層if語句嵌套是常有的事情,有什麼好的方法能夠減小嵌套呢?
一、儘早終止函數或返回數據
若是符合某個條件下能夠直接終止函數,則應該將這個條件放在第一位。咱們來看看下面的例子。

 1 if(condition1) {
 2     if(condition2){
 3         if(condition3){
 4         }
 5         else{
 6             return;
 7         }    
 8     }
 9     else{
10         return;
11     }    
12 }
13 else {
14     return;
15 }

這段代碼中if語句嵌套了3層,看起來已經很複雜了,咱們能夠將最後面的return提取到最前面去。

 1 if(!condition1){
 2     return;
 3 }
 4 if(!condition2){
 5     return;
 6 }
 7 if(!condition3){
 8     return;
 9 }
10 //doSth

這段代碼中,咱們把condition1等於false的語句提取到前面,直接終止函數,將多層嵌套的if語句重構成只有一層if語句,代碼也更清晰了。

注意:通常狀況下,咱們寫if語句會將條件爲true的狀況寫在前面,這也比較符合咱們的思惟習慣。若是是多層嵌套的狀況,應該優先減小if語句的嵌套

二、不適用if語句或switch語句
條件語句通常來講是不可避免的,有的時候,咱們要判斷不少條件就會寫不少if-elseif語句,嵌套的話,就更加麻煩了。若是有一天增長了新需求,咱們就要去增長一個if分支語句,這樣不只修改起來麻煩,並且容易出錯。《代碼大全》提出的表驅動法能夠有效地解決if語句帶來的問題。咱們來看下面這個例子:

 1 if(condition == 「case1」){
 2     return 1;
 3 }
 4 elseif(condition == 「case2」){
 5     return 2;
 6 }
 7 elseif(condition == 「case3」){
 8     return 3;
 9 }
10 elseif(condition == 「case4」){
11     return 4;
12 }

這段代碼分別依次判斷了四種狀況,若是再增長一種狀況,咱們就要再新增一個if分支,這樣就可能形成潛在的問題,如何去優化這段代碼呢?咱們能夠採用一個Map或Dictionary來將每一種狀況和相應值一一對應。

1 var map = {
2     "case1":1,
3     "case2":2,
4     "case3":3,
5     "case4":4
6 }
7 return map[condition];

經過map優化後,整個代碼不只更加簡潔,修改起來也更方便並且不易出錯了。
固然,不少時候咱們的條件判斷語句並非這麼簡單的,可能會涉及到複雜的邏輯運算,你們能夠查看《代碼大全》第18章,其中有詳細的介紹。

三、提取內層嵌套爲一個函數進行調用
多層嵌套的時候,咱們還能夠將內層嵌套提取到一個新的函數中,而後調用該函數,這樣代碼也就更清晰了。

for循環嵌套優化

for循環嵌套相比於if嵌套來講更加複雜,閱讀起來會更麻煩,下面說說幾點要注意的東西:
一、最多隻能兩層for循環嵌套
二、提取內層循環到新函數中
三、多層循環時,不要簡單地位索引變量命名爲i,j,k等,容易形成混淆,要有具體的意思

提取複雜邏輯,語義化

有的時候,咱們會寫出一些比較複雜的邏輯,閱讀代碼的人看到後可能搞不清楚要作什麼,這個時候,就應該提取出這段複雜的邏輯代碼。

1 if (age > 18 && gender == "man") {
2     //doSth
3 }

這段代碼表示當年齡大於18而且是男性的話,能夠doSth,可是仍是不夠清晰,能夠將其提取出來

1 var canDoSth = function (age, gender){
2     return age > 18 && gender == "man";
3 }
4 ...
5 ...
6 ...
7 if(canDoSth(age, gender)){
8     //doSth
9 }

雖然說多了一個函數,可是代碼更加清晰和語義化了。

總結

本文從函數命名,函數參數和函數的代碼編寫三個方面談了關於如何編寫好一個函數的感覺和想法。文中提到了不少具體的狀況,固然平常編碼中確定會遇到更多複雜的狀況可能我暫時沒有想到。我簡單的概括了幾點:
一、準確地對變量、函數命名
二、不要有重複邏輯的代碼
三、函數的行數不要超過20行,這裏的20行只是個大概,並不必定是這個數字
四、減小嵌套
我相信你們必定會不少關於這方面的經驗,歡迎進行交流,共同提升代碼質量。

 

本文地址:http://luopq.com/2016/02/21/write-good-function/,轉載請註明

相關文章
相關標籤/搜索