1. 程式人生 > >軟體設計的哲學:第十四章 選個好名字

軟體設計的哲學:第十四章 選個好名字

目錄

  • 14.1例子:不好的名字會導致錯誤
  • 14.2 創造一個形象
  • 14.3 名字要準確
  • 14.4保持一致性
  • 14.5 不同的觀點:Go style guide
  • 14.6 結論

為變數、方法和其他實體選擇名稱是軟體設計中最被低估的方面之一。好的名稱是文件的一種形式:它們使程式碼更容易理解。它們減少了對其他文件的需要,並使錯誤檢測變得更容易。 相反,糟糕的名稱選擇會增加程式碼的複雜性,產生可能導致bug的歧義和誤解。名稱選擇是複雜性遞增原則的一個例子。為特定變數選擇一個普通的名稱,而不是儘可能最好的名稱,可能不會對系統的整體複雜性產生太大的影響。然而,軟體系統有成千上萬個變數,為所有這些選擇合適的名稱將對複雜性和可管理性產生重大影響。

14.1例子:不好的名字會導致錯誤

有時,即使是一個名字不好的變數也會造成嚴重的後果。我所修復過的最具挑戰性的錯誤是由於錯誤的名字選擇。在20世紀80年代末和90年代初,我和我的研究生建立了一個分散式作業系統,稱為Sprite。在某種程度上,我們注意到檔案有時會丟失資料:一個數據塊突然變成了所有的0,即使檔案沒有被使用者修改。這個問題不常發生,所以追蹤起來特別困難。一些研究生試圖找到這個bug,但是他們無法取得進展,最終放棄了。然而,我認為任何未解決的bug都是無法忍受的人身侮辱,所以我決定跟蹤它。

雖然花了六個月的時間,但我最終還是發現並修復了這個bug。這個問題實際上非常簡單(大多數bug也是如此,只要您弄清楚它們)。檔案系統程式碼將變數名塊用於兩個不同的目的。在某些情況下,塊是指磁碟上的物理塊號;在其他情況下,塊引用檔案中的邏輯塊號。不幸的是,在程式碼中有一個塊變數包含一個邏輯塊號,但是在需要物理塊號的上下文中偶然使用了它;結果,磁碟上一個不相關的塊被0覆蓋。

在跟蹤bug的過程中,包括我在內的幾個人仔細閱讀了錯誤程式碼,但我們從未注意到這個問題。當我們看到將變數塊用作物理塊號時,我們本能地認為它確實持有物理塊號。我花了很長一段時間來測試,最終發現在一個特定的語句中肯定發生了錯誤,然後我才能夠越過這個名字所造成的心理障礙,並檢查它的價值到底來自哪裡。如果對不同型別的塊(如fileBlock和diskBlock)使用了不同的變數名,則不太可能發生錯誤,程式設計師應該知道在這種情況下不能使用fileBlock。

不幸的是,大多數開發人員沒有花很多時間考慮名稱。他們傾向於使用第一個出現在腦海中的名字,只要它與它所命名的事物相當接近。例如,塊與磁碟上的物理塊和檔案中的邏輯塊非常匹配;這當然不是一個可怕的名字。儘管如此,它還是導致了大量的時間開銷來跟蹤一個細微的bug。因此,你不應該滿足於那些“相當接近”的名字。花一些額外的時間來選擇好的名字,這些名字要準確、明確、直觀。額外的關注會很快得到回報,隨著時間的推移,你會很快學會選擇好的名字。

14.2 創造一個形象

當選擇一個名字時,目標是在讀者的腦海中創造一個關於被命名的事物本質的形象。一個好的名字傳達了很多關於底層實體是什麼的資訊,同樣重要的是,它不是什麼的資訊。 當考慮一個特定的名字時,問問你自己:“如果某人單獨看到這個名字,沒有看到它的宣告、文件或任何使用這個名字的程式碼,他們能猜出這個名字指的是什麼嗎?”有沒有別的名字能讓你更清楚地瞭解情況呢?“,當然,一個人的名字所能提供的資訊是有限的;如果名稱包含超過兩三個單詞,就會變得很笨拙。因此,我們面臨的挑戰是找到幾個詞來描述這個實體最重要的方面。

名稱是一種抽象形式:它們提供了一種簡化的方式來思考更復雜的底層實體。 與其他抽象形式一樣,最好的名稱是那些將注意力集中在底層實體最重要的方面,而忽略不太重要的細節的名稱。

14.3 名字要準確

好名字有兩個屬性:精確性和一致性。 讓我們從精度開始。名字最常見的問題是太籠統或模糊;因此,讀者很難知道這個名字指的是什麼;讀者可能會認為這個名字指的是與現實不同的東西,就像上面的block bug一樣。考慮以下方法宣告:

“計數”這個詞太泛了:計數什麼?如果有人看到這個方法的呼叫,他們不太可能知道它做了什麼,除非他們閱讀了它的文件。像getActiveIndexlets或numIndexlets這樣更精確的名稱會更好:有了這些名稱中的一個,讀者可能不用看文件就能猜出方法返回的內容。

/**

 * Returns the total number of indexlets this object is managing.

 */

int IndexletManager::getCount() {...}

以下是其他一些不夠精確的名字的例子,取自學生的各種專案:

  • 構建GUI文字編輯器的專案使用名稱x和y來表示檔案中字元的位置。這些名字太普通了。它們可能意味著許多事情;例如,它們還可以表示螢幕上字元的座標(以畫素為單位)。單獨看到名稱x的人不太可能認為它指的是字元在一行文字中的位置。如果使用charIndex和lineIndex這樣的名稱,程式碼會更清晰,這些名稱反映了程式碼實現的特定抽象。

  • 另一個編輯器專案包含以下程式碼:
// Blink state: true when cursor visible.

private boolean blinkStatus = true;

blinkStatus這個名稱並不能傳達足夠的資訊。“status”這個詞對於布林值來說太模糊了:它沒有給出關於真值或假值含義的任何線索。“blink”這個詞也很模糊,因為它並不表示什麼是blink。以下是更好的選擇:


// Controls cursor blinking: true means the cursor is visible,

// false means the cursor is not displayed.

private boolean cursorVisible = true;

cursorVisible這個名字傳達了更多的資訊;例如,它允許讀者猜測真值的含義(作為一般規則,布林變數的名稱應該總是謂詞)。名稱中不再有“blink”這個詞,因此讀者如果想知道游標為什麼不總是可見,就必須查閱文件;這個資訊不太重要。

  • 實施協商一致協議的專案包含以下程式碼:
// Value representing that the server has not voted (yet) for

// anyone for the current election term.

private static final String VOTED_FOR_SENTINEL_VALUE = "null";

這個值的名稱表明它是特殊的,但它沒有說明特殊的含義是什麼。更具體的名稱如not_yet_會更好。

  • 在沒有返回值的方法中使用了一個名為result的變數。這個名稱有多個問題。首先,它會造成一種誤導,即它將是方法的返回值。其次,它基本上不提供關於它實際持有的內容的任何資訊,除了它是某個計算值之外。名稱應該提供關於實際結果的資訊,如mergedLine或totalChars。在確實具有返回值的方法中,使用名稱result是合理的。這個名稱仍然有點泛型,但是讀者可以檢視方法文件來了解它的含義,瞭解這個值最終將成為返回值是很有幫助的。

危險訊號:模糊的名字

如果一個變數或方法名足夠寬泛,可以引用許多不同的東西,那麼它就不能向開發人員傳遞太多資訊,底層實體更有可能被誤用。

與所有規則一樣,選擇精確名稱的規則也有一些例外。例如,只要迴圈只跨幾行程式碼,就可以使用i和j之類的通用名稱作為迴圈迭代變數。如果您可以看到一個變數的整個使用範圍,那麼從程式碼中就可以看出該變數的含義,因此不需要很長的名稱。例如,考慮以下程式碼:

for  (i = 0; i < numLines; i++){      
    ...
}

從這段程式碼可以清楚地看出,i是用來遍歷某個實體中的每一行的。如果迴圈太長,您無法一次全部看到它,或者如果很難從程式碼中找出迭代變數的含義,那麼應該使用更具描述性的名稱。

名稱也可能過於具體,比如在這個宣告中,一個方法刪除了一個範圍的文字:

void delete(Range selection) {
    ...
}

引數名的選擇太具體了,因為它表明被刪除的文字總是在使用者介面中被選擇。但是,可以在選定的或未選定的任何文字範圍上呼叫此方法。因此,引數名應該更通用,比如range。

如果您發現很難為一個特定的變數找到一個精確、直觀、不太長的名稱,那麼這就是一個危險訊號。這表明該變數可能沒有明確的定義或目的。當這種情況發生時,考慮替代因素。例如,您可能試圖使用單個變數來表示多個事物;如果是這樣,將表示分離成多個變數可能會使每個變數的定義更簡單。選擇好名字的過程可以通過識別弱點來改進設計。

危險訊號:選擇很難的名字

如果很難為建立底層物件的清晰影象的變數或方法找到一個簡單的名稱,那麼這就暗示底層物件可能沒有一個乾淨的設計。

14.4保持一致性

好名字的第二個重要屬性是一致性。 在任何程式中,都有一些反覆使用的變數。例如,檔案系統反覆操作塊號。對於這些常見用法,選擇一個用於此目的的名稱,並在任何地方使用相同的名稱。例如,檔案系統可能總是使用fileBlock來儲存檔案中塊的索引。一致的命名減少了認知負擔,其方式與重用公共類非常相似:一旦讀者在一個上下文中看到了名稱,他們就可以重用自己的知識,並在不同上下文中看到名稱時立即做出假設。

一致性有三個要求:第一,總是為給定的目的使用通用名稱;第二,不要把普通的名字用在與特定目的無關的任何事情上;第三,確保目的足夠狹窄,所有具有名稱的變數具有相同的行為。 這第三個要求在本章開頭的檔案系統錯誤中被違反了。檔案系統對具有兩種不同行為(檔案塊和磁碟塊)的變數使用塊;這導致了對變數含義的錯誤假設,從而導致了一個bug。

有時你會需要多個變數來表示相同的東西。例如,複製檔案資料的方法需要兩個塊號,一個用於源,一個用於目標。當發生這種情況時,為每個變數使用公共名稱,但新增一個可區分的字首,如srcFileBlock和dstFileBlock。

迴圈是一致命名可以提供幫助的另一個領域。如果您對迴圈變數使用i和j之類的名稱,則始終在最外層的迴圈中使用i,而在巢狀迴圈中使用j。這允許讀者在看到給定名稱時立即(安全地)假設程式碼中發生了什麼。

14.5 不同的觀點:Go style guide

並不是每個人都同意我對命名的看法。一些圍棋語言的開發者認為名字應該很短,通常只有一個字元。在一場關於Go名字選擇的演講中,Andrew Gerrand指出“冗長的名字掩蓋了程式碼的作用。”他給出了這個程式碼示例,它使用了單字母變數名:

func RuneCount(b []byte) int {
       i, n := 0, 0
       for i < len(b) {
             if b[i] < RuneSelf {
                   i++
             } else {
                   _, size := DecodeRune(b[i:])
                   i += size
             }
             n++
       }
       return n
}

並認為它比下面的版本可讀性更強,下面的版本使用了更長的名稱:

func RuneCount(buffer []byte) int {
       index, count := 0, 0
       for index < len(buffer) {
             if buffer[index] < RuneSelf {
                   index++
             } else {
                   _, size := DecodeRune(buffer[index:])
                   index += size
             }
             count++
       }
       return count
}

就我個人而言,我並不覺得第二個版本比第一個版本更難讀。如果有什麼不同的話,那麼name count對於變數的行為提供了比n更好的線索。在第一個版本中,我最後通讀了程式碼,試圖找出n的含義,而在第二個版本中,我覺得沒有必要這樣做。但是,如果在整個系統中始終使用n來引用count(而不是其他任何東西),那麼其他開發人員可能會清楚這個簡短的名稱。

Go文化鼓勵對多個不同的事物使用相同的短名稱;ch表示字元或通道,d表示資料、差異或距離,等等。對我來說,像這樣的模糊名稱可能會導致混淆和錯誤,就像塊示例中那樣。

總的來說,我認為可讀性必須由讀者決定,而不是作者。如果您編寫的程式碼具有簡短的變數名,並且讀它的人發現它很容易理解,那麼這是可以的。如果您開始收到關於您的程式碼太過神祕的抱怨,那麼您應該考慮使用更長的名稱(在Web上搜索“go語言的短名稱”將識別出幾個這樣的抱怨)。類似地,如果我開始收到抱怨,說較長的變數名使我的程式碼難以閱讀,那麼我將考慮使用較短的變數名。

Gerrand做了一個我同意的評論:“一個名字的宣告和它的使用之間的距離越大,這個名字就應該越長。”前面關於使用迴圈變數i和j的討論就是這個規則的一個例子。

14.6 結論

精心選擇的名稱有助於使程式碼更明。當一個人第一次遇到這個變數時,他們對它的行為的第一個猜測是正確的。 選擇好的名字是在第3章中討論的投資心態的一個例子:如果你提前花一點額外的時間來選擇好的名字,將來在程式碼上工作就會更容易。此外,您將不太可能引入bug。開發一種命名技能也是一項投資。當你第一次決定不再滿足於平庸的名字,你可能會發現它令人沮喪和耗時想出好名字。然而,當你獲得更多的經驗,你會發現它變得更容易。最終,你會發現,選擇好名字幾乎不需要額外的時間,所以你幾乎可以免費獲得這些好