提高程式碼質量:如何編寫函式

狼狼的藍胖子發表於2016-02-24

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

命名

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

採用統一的命名規則

在談及如何為函式取一個準確而優雅的名字之前,首先最重要的是要有統一的命名規則。這是提高程式碼可讀性的最基礎的準則。
帕斯卡命名法和駝峰命名法是目前比較流行的兩種規則,不同語言採用的規則可能不一樣,但是要記住一點:保持團隊和個人風格一致。

1、帕斯卡命名法

帕斯卡命名法簡單地說就是:多個單片語成一個名稱時,每個單詞的首字母大寫。比如:

public void SendMessage ();
public void CalculatePrice ();

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

2、駝峰命名法

駝峰命名法和帕斯卡命名法很類似,多個單片語成一個名稱時,第一個單詞全部小寫,後面單詞首字母大寫。比如:

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

駝峰命名法一般用於欄位、區域性變數、函式引數等等。在JS中,函式也常用此方法命名。

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

儘可能完整地描述函式所做的所有事情

有的開發者可能覺得相較於長函式名來說,短函式名看起來可能更簡潔,看起來也更舒服。但是通常來說,函式名稱越短其描述的意思越抽象。函式使用者對函式的第一印象就是函式名稱,進而瞭解函式的功能,我們應該儘可能地描述到函式所做的所有事情,防止使用者不知道或誤解造成潛在的錯誤。

舉個例子,假設我們做一個新增評論的功能,新增完畢後並返回評論總數量,如何命名比較合適呢?

// 描述不夠完整的函式名
var count = function addComment() {};

// 描述完整的函式名
var count = function addCommentAndReturnCount() {};

這只是簡單的一個例子,實際開發中可能會遇到得更多複雜的情況,單一職責原則是我們開發函式要遵守的準則,但是有時候無法做到函式單一職責時,請記得函式名應該儘可能地描述所有事情。當你無法命名一個函式時,應該分析一下,這個函式的編寫是否科學,有什麼辦法可以去最佳化它。

採用準確的描述動詞

這一點對母語非英語的開發者來說應該是比較難的一點,想要提高這方面的能力,最主要的還是要提高詞彙量,多閱讀優秀程式碼積累經驗。

這裡簡單說說我自己的一些感想和看法:

1、不要採用太抽象廣泛的單詞

很多開發人員會採用一個比較寬泛的動詞來為函式命名,最典型的一個例子就是get這個單詞。我們平時開發中經常會透過各種不同的方式拿到資料,但是每一種方式都用get就有點太抽象了。具體如何命名,要具體分析:

(1)簡單的返回資料

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

(2)從遠端獲取資料

var fetchPersons = function () {
    ...
    $.ajax({
    })
}

(3)從本地儲存載入資料

var loadPersons = function () {};

(4)透過計算獲取資料

var calculateTotal = function () {};

(5)從陣列中查詢資料

var findSth = function (arr) {};

(6)從一些資料生成或得到

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

這是一個簡單的例子,我們平時開發中遇到的情況肯定會複雜得多,關鍵還是靠單詞的積累,多閱讀優秀原始碼

下面是整理的一些常用的對仗詞,大家可以參考使用

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

根據不同專案和需求制定好命名規則

這一點也是很重要的,尤其是在團隊合作中,不同的專案和需求可能導致的不同的命名規則。

比如我們通常採用的命名規則是動賓結構,也就是動詞在前,名詞災後。但是有一些專案,比如資料介面等專案中,有的團隊會採用名字在前,動詞在後的形式,例如:

public static Product[] ProductsGet(){};
public static Product[] ProductsDel(){};
public static Customer[] CustomerDel(){};
public static Customer[] CustomerDel(){};

這種的好處是看到前面的名詞,比如ProductsGet,就能很快的知道這是產品相關的資料介面。
當然這個並不是絕對的,關鍵還是要團隊共同制定和遵守同一套命名規則。

函式引數

函式使用者在呼叫函式時,必須嚴格遵守函式定義的引數,這對函式的易用性,可測試性等方面都是至關重要的。下面我從幾個方面來談談關於如何最佳化好函式引數的一些想法。

引數數量

毫無疑問,函式引數越多,函式的易用性就越差,因為使用者需要嚴格眼中引數列表依次輸入引數,如果某個引數輸錯,將導致不可意料的結果。

但是,函式引數就一定越少越好嗎?我們來看看下面的例子:

var count = 0;
var unitPrice = 1.5;
....
...
var calculatePrice = function () {
    return count * unitPrice;
}

在這個例子中,我們透過calculatePrice這個函式來計算價格,函式不接收任何引數,直接透過兩個全域性變數unitPrice和count進行計算。這種函式的定義對使用者來說非常方便,直接呼叫即可,不用輸入任何引數。但是這裡可能會有潛在的bug:全域性變數可能在其他地方被修改成其他值了,難以進行單元測試等等問題。所以,這個函式可以傳入數量和價格資訊:

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

這種方式下,函式使用者在使用時,要傳入引數進行呼叫,避免了全域性變數可能存在的問題。另外也降低了耦合,提高了可測試性,在測試的時候就不必依賴於全域性變數。

當然,在保證函式不依賴於全域性變數和測試性的情況下,函式引數還是越少越好。《程式碼大全》中提出將函式的引數限制在7個以內,這個可以作為我們的參考。

有的時候,我們不可避免地要使用超過10個以上函式,在這中情況下,我們可以考慮將類似的引數構造成一個類,我們來看看一個典型的例子。

我相信大家平時一定做過這樣的功能,列表篩選,其中涉及到各種條件的篩選,排序,分頁等等功能,如果將引數一個一個地列出來必定會很長,例如:

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

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

function DatePlace (city, checkIn, checkOut){
    this.city = city;
    this.checkIn = checkIn;
    this.checkOut = checkOut
}

function HotelFeature (price, star, position, wifi, meal){
    this.price = price;
    this.star = star;
    this.position = position;
    this.wifi = wifi;
    this.meal = meal;
}

var filterHotel = function (datePlce, hotelFeature, sort, pageIndex) {};

將多個引數提取成物件了,雖然物件數量增多了,但是函式引數更清晰了,呼叫起來也更方便了。

儘量不要使用bool型別作為引數

有的時候,我們會寫出使用bool作為引數的情況,比如:

var getProduct = function(finished) {
    if(finished){
    }
    else{
    }
}

// 呼叫
getProduct(true);

如果沒有註釋,使用者看到這樣的程式碼:getProduct(true),他肯定搞不清楚true是代表什麼意思,還要去檢視函式定義才能明白這個函式是如何使用的。這就意味著這個函式不夠清晰,就應該考慮去最佳化它。通常有兩種方式去最佳化它:

  1. 將函式一分為二,分成兩個函式getFinishedProduct和getUnFinishedProduct
  2. 將bool轉換成有意義的列舉getProduct(ProductStatus)

不要修改輸入引數

如果輸入引數在函式內被修改了,很有可能造成潛在的bug,而且使用者不知道呼叫函式後居然會修改函式引數。
正確使用輸入引數的做法應該是隻傳入引數用於函式呼叫。

如果不可避免地要修改,一定要在註釋中說明。

儘量不要使用輸出引數

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

編寫函式體

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

相關操作放在一起

有的時候,我們會在一個函式內進行一系列的操作來完成一個功能,比如:

var calculateTotalPrice = function()  {
    var roomCount = getRoomCount();
    var mealCount = getMealCount();

    var roomPrice = getRoomPrice(roomCount);
    var mealPrice = getMealPrice(mealCount);

    return roomPrice + mealPrice;
}

這段程式碼計算了房間價格和早餐價格,然後將兩者相加返回總價格。

這段程式碼乍一看,沒有什麼問題,但是我們分析程式碼,我們先是分別獲取了房間數量和早餐數量,然後再透過房間數量和早餐數量分別計算兩者的價格。這種情況下,房間數量和計算房間價格的程式碼分散在了兩個位置,早餐價格的計算也是分散到了兩個位置。也就是兩部分相關的程式碼分散在了各處,這樣閱讀起程式碼來邏輯會略顯不通,程式碼組織不夠好。我們應該讓相關的語句和操作放在一起,也有利於重構程式碼。我們修改如下:

var calculateTotalPrice = function()  {
    var roomCount = getRoomCount();
    var roomPrice = getRoomPrice(roomCount);

    var mealCount = getMealCount();
    var mealPrice = getMealPrice(mealCount);

    return roomPrice + mealPrice;
}

我們將相關的操作放在一起,這樣程式碼看起來更清晰了,而且也更容易重構了。

儘量減少程式碼巢狀

我們平時寫if,switch或for語句是常有的事兒,也一定寫過多層if或for語句巢狀的情況,如果程式碼裡的巢狀超過3層,閱讀起來就會非常困難了。我們應該儘量避免程式碼巢狀多層,最好不要超過2層。下面我來說說我平時一些減少巢狀的技巧或方法。

if語句巢狀的問題

多層if語句巢狀是常有的事情,有什麼好的方法可以減少巢狀呢?

1、儘早終止函式或返回資料

如果符合某個條件下可以直接終止函式,則應該將這個條件放在第一位。我們來看看下面的例子。

if(condition1) {
    if(condition2){
        if(condition3){
        }
        else{
            return;
        }    
    }
    else{
        return;
    }    
}
else {
    return;
} 

這段程式碼中if語句巢狀了3層,看起來已經很複雜了,我們可以將最後面的return提取到最前面去。

if(!condition1){
    return;
}
if(!condition2){
    return;
}
if(!condition3){
    return;
}
//doSth

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

注意:一般情況下,我們寫if語句會將條件為true的情況寫在前面,這也比較符合我們的思維習慣。如果是多層巢狀的情況,應該優先減少if語句的巢狀

2、不適用if語句或switch語句

條件語句一般來說是不可避免的,有的時候,我們要判斷很多條件就會寫很多if-elseif語句,巢狀的話,就更加麻煩了。如果有一天增加了新需求,我們就要去增加一個if分支語句,這樣不僅修改起來麻煩,而且容易出錯。《程式碼大全》提出的表驅動法可以有效地解決if語句帶來的問題。我們來看下面這個例子:

if(condition == “case1”){
    return 1;
}
elseif(condition == “case2”){
    return 2;
}
elseif(condition == “case3”){
    return 3;
}
elseif(condition == “case4”){
    return 4;
}

這段程式碼分別依次判斷了四種情況,如果再增加一種情況,我們就要再新增一個if分支,這樣就可能造成潛在的問題,如何去最佳化這段程式碼呢?我們可以採用一個Map或Dictionary來將每一種情況和相應值一一對應。

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

透過map最佳化後,整個程式碼不僅更加簡潔,修改起來也更方便而且不易出錯了。

當然,很多時候我們的條件判斷語句並不是這麼簡單的,可能會涉及到複雜的邏輯運算,大家可以檢視《程式碼大全》第18章,其中有詳細的介紹。

3、提取內層巢狀為一個函式進行呼叫

多層巢狀的時候,我們還可以將內層巢狀提取到一個新的函式中,然後呼叫該函式,這樣程式碼也就更清晰了。

for迴圈巢狀最佳化

for迴圈巢狀相比於if巢狀來說更加複雜,閱讀起來會更麻煩,下面說說幾點要注意的東西:

  1. 最多隻能兩層for迴圈巢狀
  2. 提取內層迴圈到新函式中
  3. 多層迴圈時,不要簡單地位索引變數命名為i,j,k等,容易造成混淆,要有具體的意思

提取複雜邏輯,語義化

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

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

這段程式碼表示當年齡大於18並且是男性的話,可以doSth,但是還是不夠清晰,可以將其提取出來

var canDoSth = function (age, gender){
    return age > 18 && gender == "man";
}
...
...
...
if(canDoSth(age, gender)){
    //doSth
}

雖說多了一個函式,但是程式碼更加清晰和語義化了。

總結

本文從函式命名,函式引數和函式的程式碼編寫三個方面談了關於如何編寫好一個函式的感受和想法。文中提到了很多具體的情況,當然日常編碼中肯定會遇到更多複雜的情況可能我暫時沒有想到。我簡單的歸納了幾點:

  1. 準確地對變數、函式命名
  2. 不要有重複邏輯的程式碼
  3. 函式的行數不要超過20行,這裡的20行只是個大概,並不一定是這個數字
  4. 減少巢狀

我相信大家一定會很多關於這方面的經驗,歡迎進行交流,共同提高程式碼質量。

相關文章