為變量、方法和其他實體選擇名稱是軟件設計中最被低估的方面之一。 好的名稱是文檔的一種形式:它們使代碼更容易理解。它們減少了對其他文檔的需要,並使錯誤檢測變得更容易。 相反,糟糕的名稱選擇會增加代碼的復雜性,產生可能導致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語言的開發者認為名字應該很短,通常只有一個字符。在一場關於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。開發一種命名技能也是一項投資。當你第一次決定不再滿足於平庸的名字,你可能會發現它令人沮喪和耗時想出好名字。然而,當你獲得更多的經驗,你會發現它變得更容易。最終,你會發現,選擇好名字幾乎不需要額外的時間,所以你幾乎可以免費獲得這些好處。