天天看點

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

為變量、方法和其他實體選擇名稱是軟體設計中最被低估的方面之一。好的名稱是文檔的一種形式:它們使代碼更容易了解。它們減少了對其他文檔的需要,并使錯誤檢測變得更容易。相反,糟糕的名稱選擇會增加代碼的複雜性,産生可能導緻bug的歧義和誤解。

目錄

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

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

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

雖然花了六個月的時間,但我最終還是發現并修複了這個bug。這個問題實際上非常簡單(大多數bug也是如此,隻要您弄清楚它們)。檔案系統代碼将變量名塊用于兩個不同的目的。在某些情況下,塊是指磁盤上的實體塊号;在其他情況下,塊引用檔案中的邏輯塊号。不幸的是,在代碼中有一個塊變量包含一個邏輯塊号,但是在需要實體塊号的上下文中偶然使用了它;結果,磁盤上一個不相關的塊被0覆寫。

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

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

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

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

好名字有兩個屬性:精确性和一緻性。 讓我們從精度開始。名字最常見的問題是太籠統或模糊;是以,讀者很難知道這個名字指的是什麼;讀者可能會認為這個名字指的是與現實不同的東西,就像上面的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。

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

危險信号:選擇很難的名字

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

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

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

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

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

并不是每個人都同意我對命名的看法。一些GO語言的開發者認為名字應該很短,通常隻有一個字元。在一場關于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的讨論就是這個規則的一個例子。

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

關注公衆号:架構未來 ,我們一起學習成長

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

繼續閱讀