第 14 章 選取名稱
為變數、方法和其他實體選擇名稱是軟體設計中最被低估的方面之一。好的名稱是一種形式的文件:它們使程式碼更易於理解。它們減少了對其他文件的需求,並使檢測錯誤更加容易。相反,名稱選擇不當會增加程式碼的複雜性,造成的歧義和誤解可能會導致程式碼缺陷。名稱選擇是複雜性逐步累積的原因之一。為特定變數選擇一個平庸的名稱,而不是最好的名稱,可能不會對系統的整體複雜性產生太大影響。但是,軟體系統具有成千上萬個變數,為所有這些選擇好的名稱將對複雜性和可管理性產生重大影響。
14.1 示例:壞名稱會導致程式碼缺陷
有時,即使是一個取名不當的變數也會產生嚴重的後果。我曾經修復過的最具挑戰性的程式碼缺陷就是由於名稱選取不當造成的。在 1980 年代末和 1990 年代初,我的研究生和我建立了一個名為 Sprite 的分散式作業系統。在某個時候,我們注意到檔案偶爾會丟失資料:即使使用者未修改檔案,資料塊之一也會突然變為全零。該問題並不經常發生,因此很難追蹤。一些研究生試圖找到該錯誤,但他們未能取得進展,最終放棄了。但是,我認為任何未解決的程式碼缺陷都是無法忍受的,因此我決定對其進行跟蹤。
結果花了六個月的時間,但我最終找到並修復了該缺陷。這個問題實際上很簡單(就像大多數缺陷一樣,一旦您找出原因之後)。檔案系統程式碼將變數名 block
用於兩個不同的目的。在某些情況下,block
是指磁碟上的物理塊號;在其它情況下,block
是指檔案中的邏輯塊號。不幸的是,在程式碼的某處有一個包含邏輯塊號的塊變數,但是在某個需要物理塊號的情況下意外地使用了它。結果,磁碟上無關的塊被重置為零了。
在跟蹤該錯誤時,包括我自己在內的幾個人都閱讀了有問題的程式碼,但我們從未注意到問題所在。當我們看到變數 block
用作物理塊號時,我們本能地假設它確實擁有物理塊號。經過很長時間的排查,最終表明損壞一定發生在特定的語句中,然後我才能越過該名稱所建立的思維障礙,並檢查它的值究竟來自何處。如果對不同型別的塊使用不同的變數名(例如 fileBlock
和 diskBlock
),則錯誤很可能不會發生;程式設計師會知道在哪種情況下不能使用 fileBlock
。甚至更好的是給這兩種不同的塊定義不同的型別,這樣它們就不可能互換。
不幸的是,大多數開發人員沒有花太多時間在思考名稱上面。他們傾向於使用想到的第一個名稱,只要它的含義與被命名的事物合理相近即可。例如,塊與磁碟上的物理塊和檔案內的邏輯塊都非常接近,這肯定不是一個可怕的名稱。即使如此,它還是導致花費了大量時間來追蹤一個細微的錯誤。因此,您不應該只選擇“合理相近”的名稱。花一些額外的時間來選擇準確、明確且直觀的好名稱。額外的時間花費將很快收回成本,隨著時間的流逝,您將學會快速選擇好名稱。
14.2 創造畫面
選擇名稱時,目標是能在讀者的腦海中創造出關於被命名事物的本質的畫面。一個好的名稱傳達了很多有關底層實體是什麼以及(同樣重要的)不是什麼的資訊。在考慮一個特定的名稱時,請問自己:“如果有人孤立地看到這個名稱,而沒有看到其宣告、文件或使用該名稱的任何程式碼,他們是否能夠猜到該名稱指的是什麼?還有其他名稱可以讓這個畫面更清晰嗎?” 當然,一個名稱可以包含多少資訊是有限制的。如果名稱包含兩個或三個以上的單詞,則會變得笨拙。因此,挑戰是僅通過幾個單詞就能捕獲到實體的最重要的方面。
名稱是一種抽象形式:名稱提供了一種簡化的方式來思考更複雜的底層實體。與其他抽象形式一樣,最好的名稱會突出底層實體最重要的東西,而忽略那些次要的細節。
14.3 名稱應該是精確的
良好的名稱具有兩個屬性:精確性和一致性。讓我們從精確性開始。名稱最常見的問題是太籠統或含糊不清。因此,讀者很難說出這個名稱指的是什麼。讀者可能會認為該名稱所指的是與現實不符的事物,如上面的 block
缺陷所示。考慮以下方法宣告:
/**
* Returns the total number of indexlets this object is managing.
*/
int IndexletManager::getCount() {...}
術語 count
太籠統了:對什麼計數?如果有人看到此方法的呼叫,除非他們閱讀了它的文件,否則他們不太可能知道它的作用。像 numActiveIndexlets
這樣的更精確的名稱會更好:很多讀者可能無需檢視其文件就能猜測該方法返回的內容。
危險訊號:模糊的名稱
如果變數或方法的名稱足夠廣泛,可以指代許多不同的事物,那麼它不會向開發人員傳遞太多資訊,因此其底層的實體很可能會被誤用。
以下是其他一些來自學生專案的名稱不夠精確的示例:
構建影像介面文字編輯器的專案使用名稱
x
和y
來表示字元在檔案中的位置。這些名稱太籠統了。他們可能意味著很多事情。例如,它們也可能代表螢幕上字元的座標(以畫素為單位)。單獨看到名稱x
的人不太可能會認為它是指字元在一行文字中的位置。如果使用諸如charIndex
和lineIndex
之類的名稱來反映程式碼實現的特定抽象,該程式碼將更加清晰。另一個編輯器專案包含以下程式碼:
// Blink state: true when cursor visible. private boolean blinkStatus = true;
blinkStatus
這個名稱無法傳達足夠的資訊。status
一詞對於布林值來說太含糊了:它不提供關於真值或假值含義的任何線索。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_VOTED
)會更好。在沒有返回值的方法中使用了名為
result
的變數。這個名稱有多個問題。首先,它會產生誤導,讓人以為它將成為方法的返回值。其次,它除了是某種計算值外,實際上沒有提供關於持有內容的任何資訊。它的名稱應提供有關result
實際是什麼的資訊,例如mergedLine
或totalChars
。在實際上確實具有返回值的方法中,使用result
名稱是合理的。該名稱仍然有點通用,但是讀者可以檢視方法的文件以瞭解其含義,這有助於知道什麼值最終將成為返回值。Linux 核心包含兩個描述網路套接字結構的結構:
struct socket
和struct sock
。struct sock
包含一個struct socket
作為其第一個元素,它實際上是struct socket
的子類。這些名稱如此相似,以至於很難記住哪個是哪個。選擇易於區分的並闡明瞭這兩個型別之間的關係的名稱會更好,例如struct sock_base
和struct inet_sock
。
像所有規則一樣,有關選擇精確名稱的規則也有一些例外。例如,如果迴圈僅包含幾行程式碼,也可以將通用名稱(如 i
和 j
)用作迴圈迭代變數。如果您可以直接看到一個變數的完整使用範圍,那麼該變數的含義可能在程式碼中就很明顯了,因此您不需要長名稱。例如以下程式碼:
for (i = 0; i < numLines; i++) {
...
}
從這段程式碼中可以很明顯地看到 i
正被用來迭代某個實體中的每一行。如果迴圈太長,以至於您無法一次看到全部內容,或者如果很難從程式碼中找出迭代變數的含義,那麼應該使用更具描述性的名稱。
名稱也可能太具體,例如這個用來刪除文字範圍的方法的申明:
void delete(Range selection) {...}
selection
引數的名稱過於具體,因為它暗示要刪除的文字是當前在使用者介面中選取的。但是,可以在任意範圍的文字上呼叫此方法,無論是否選取。因此,這個引數名稱應選取更通用的,例如 range
。
如果您發現很難為特定變數想出一個精確、直觀且不太長的名稱,那麼這是一個危險訊號。這表明該變數可能沒有清晰的定義或目的。發生這種情況時,請考慮其它因素。例如,也許您正在嘗試使用單個變數來表示多個事物;如果是這樣,將這種表示分成多個變數可能會讓每個變數的定義更簡單。選取好名稱的過程可以透過識別弱點來改善您的設計。
危險訊號:難以選取名稱
如果很難為變數或方法找到一個簡單的名稱,該名稱能夠清晰地描述底層物件,那麼這暗示底層物件的設計可能不夠簡潔。
14.4 命名要確保一致性
好的名稱的第二個重要屬性是一致性。在任何程式中,都會反覆使用某些變數。例如,檔案系統反覆操作塊號。對於每種常見用途,請選擇一個用於該目的的名稱,並在各處使用相同的名稱。例如,檔案系統可能總是使用 fileBlock
來儲存檔案中的塊索引。一致的命名方式與複用一個通用的類一樣,可以減輕認知負荷:一旦讀者在一個上下文中看到了該名稱,當他們在不同上下文中看到該名稱時,就可以重用其知識並立即做出假設。
一致性具有三個要求:首先,始終將通用名稱用於給定目的;其次,除了給定目的外,切勿使用通用名稱;第三,確保給定的目的足夠窄,以使所有具有該名稱的變數都具有相同的行為。在本章開頭的檔案系統缺陷案例中違反了第三項要求。檔案系統使用 block
來表示具有兩種不同行為的變數(檔案塊和磁碟塊),這導致對變數含義的錯誤假設,進而導致程式碼缺陷。
有時您將需要多個變數來引用相同的事物。例如,一個複製檔案資料的方法將需要兩個塊號,一個為源,一個為目標。發生這種情況時,請對每個變數使用通用名稱,但要新增一個可區分的字首,例如 srcFileBlock
和 dstFileBlock
。
迴圈是一致性命名可以提供幫助的另一個領域。如果將諸如 i
和 j
之類的名稱用於迴圈變數,則始終在最外層迴圈中使用 i
,而在巢狀的迴圈中始終使用 j
。這使讀者可以在看到給定名稱時對程式碼中發生的事情做出即時的(安全的)假設。
14.5 避免多餘的單詞
名稱中的每一個單詞都應該提供有用的資訊,沒有幫助澄清變數含義的單詞只會增加混亂(例如,它們可能會導致更多的行要換行)。一個常見的錯誤是向名稱中新增通用的名詞,例如 field
或 object
,例如 fileObject
。在這種情況下,單詞 Object
可能不會提供有用的資訊(還有不是物件的檔案嗎?),因此應該從名稱中省略。
一些編碼風格會在名稱中包含型別資訊,例如 filePtr
表示一個指向檔案物件的指標變數。一個極端的例子是在微軟的 C 程式設計中使用的匈牙利命名法。在匈牙利命名法中,每個變數名稱都有一個字首,表示其完整的型別。例如,名稱 arru8NumberList
表示該變數是一個無符號 8 位整數的陣列。儘管我過去也會在變數名稱中包含型別資訊,但不再推薦這樣做。隨著現代 IDE 的出現,很容易從變數名稱跳轉到其宣告(或者 IDE 甚至可以自動顯示型別資訊),因此不需要在變數名稱中包含此資訊。
另一個多餘單詞的例子是當一個類的例項變數重複了類的名稱時,例如在名為 File 的類中有一個名為 fileBlock 的例項變數。該變數是 File 類的組成部分在上下文中很明顯,因此將類名包含在變數名稱中沒有提供任何有用的資訊,只需將變數命名為 block
(除非該類包含多個不同型別的塊)。
14.6 不同的觀點:Go 樣式指南
並非所有人都同意我對命名的看法。一些使用 Go 語言的開發人員認為,名稱應該非常簡短,通常只能是一個字元。在關於 Go 的名稱選擇的演示中,Andrew Gerrand 指出“長名稱模糊了程式碼的作用。” [1] 他介紹了此程式碼示例,該示例使用單字母變數名:
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
}
就個人而言,我不覺得第二版比第一版更難讀。比如,與 n
相比,名稱 count
為變數的行為提供了更好的線索。在第一個版本中,我最終通讀了程式碼,才弄清楚 n
的含義,而第二個版本中我並沒有這種需要。但是,如果在整個系統中一致地使用 n
來表示計數(而不表示任何其它內容),那麼其他開發人員可能會清楚知道該短名稱。
Go 文化鼓勵在多個不同的事物上使用相同的短名稱:ch
用於字元或通道,d
用於資料、差異或距離,等等。對我來說,像這樣的模稜兩可的名稱很可能導致混亂和錯誤,就像在檔案塊的示例中一樣。
總的來說,我認為可讀性必須由讀者而不是作者來決定。如果您使用簡短的變數名編寫程式碼,並且閱讀該程式碼的人很容易理解,那麼很好。如果您開始抱怨程式碼很含糊,那麼您應該考慮使用更長的名稱(在網路上搜索 go language short name
會發現一些這樣的抱怨)。同樣,如果我開始抱怨長變數名使我的程式碼難以閱讀,那麼我會考慮使用較短的變數名。
Gerrand 發表了一個我同意的評論:“名稱宣告與其使用之間的距離越大,名稱就應該越長。” 前面關於使用名為 i
和 j
的迴圈變數的討論是此規則的示例。
14.7 結論
精心選取的名稱能大大提高程式碼的可讀性。當有人第一次遇到該變數時,他們對行為的第一次猜測就是正確的,而不需要太多的思考。選取好名稱是第 3 章討論的投資思維的一個示例:如果您花一些額外的時間來選取好名稱,將來您將更容易處理程式碼。此外,您引入程式碼缺陷的可能性更小。培養命名技巧也是一項投資。當您第一次決定不再滿足於平庸的名稱時,您會發現想出好名稱的過程既令人沮喪又耗時。但是,隨著您獲得更多的經驗,您會發現命名變得更加容易。最終,您將幾乎不需要花費額外的時間來選取好名稱,因此您幾乎可以毫不費力地獲得它的好處。
[1] https://talks.golang.org/2014/names.slide#1