程式設計寶典(二)命名的藝術

Rogerh.eth
誌瓜筆記
Published in
8 min readDec 10, 2019

前言

命名在程式開發裡無處不見,我們替變數、函式、參數、類別和套件命名,也替程式原始檔命名…總之,在軟體開發的過程中,命名是必不可少的的一件事,它看似很容易,也不需要特別去學習,但在本文中我會透過一個一個實例,讓你深刻的體會到好的命名究竟有多重要,接下我將介紹一些良好命名的規則。

使「命名」名符其實

相比於我一直強調強調命名的重要性,讓讀者能夠自己體會到、感受到其重要性,我更喜歡後者,因此接下來我會用一些實例讓讀者能夠看出良好命名與不好命名的差異:

想像我們要開發一款踩地雷遊戲,有一個程式設計師,給了你以下代碼:

public List<int[]> getThem () {
List<int[]> = new ArrayList<int[]> ();
for (int[] x : theList)
if (x[0] == 4)
list1.add (x);
return list1;
}

我想如果我問你們這段程式在做什麼,答案應該相當容易,以上這段程式碼並不包含什麼複雜的運算式,空格與縮排等等也都沒什麼問題,也只使用到了兩個常數與三個變數,沒有其他的類別和方法,只是一個陣列的列表而已。

但如果我要讀者說出這段程式碼要做什麼,應該沒有人能答的出來。為什麼?問題在於程式碼的隱含性(implicity),也就是上述程式碼應該要能回答以下問題:

💡隱含性:表示程式的上下文資訊能夠由程式本身明確地展現出來的程度。
  1. theList 中存放什麼類型的東西?
  2. theList 裡面索引為 0 的項目代表什麼?
  3. 數字 4 的意義是什麼?
  4. 該如何使用回傳的列表?

如果讀者嘗試回答這些問題,應該能夠感覺到以上程式並沒有做到明確的說明這些問題的答案,那我們應該如何改善這段程式碼呢?

在我明確的詢問了以上代碼的意義後,我將程式碼修改如下:

public List<Cell> getFlaggedCells () {
List<Cell> flaggedCells = new ArrayList<int[]> ();
for (Cell cell : gameBoard)
if (cell[STATUS_VALUE] == FLAGGED)
flaggedCells.add (cell);
return flaggedCells;
}
  • 踩地雷盤面應是由連續儲存格所構成,因此將 theList 重新命名為 gameBoard。
  • 索引 0 是用來表示所在地雷格的狀態值,若等於 4 則代表此地雷格被「插旗」,因此將 if 的條件修改為 cell[STATUS_VALUE] == FLAGGED
  • 將地雷格寫成一個簡單的類別 Cell 取代原來的 int[] 來清楚表明 gameBoard 就是由一個個地雷個所組成的遊戲盤面。

修改完後,讀者應該能夠發現程式碼的簡易度、複雜度並未改變,但卻明確了很多,清楚地闡明了整段程式碼到底要做什麼,這個例子我想也足以顯現好的命名帶來的優勢。

避免誤導

對於程式開發者,根據自己習慣的用法來命名是難以避免的,但習慣並非隨便,在為任何東西命名時,你都必須謹記一個原則:

你的程式碼不只是給自己看,你希望看到別人給你什麼樣的程式碼,你就應該寫出什麼樣的程式碼。

舉一個簡單的例子, hp 就是一個不恰當的命名,因為他非常容易造成誤導,經常接觸 UNIX 平台的會認為這個變數可能是代表 UNIX 平台或其他類似平台的名稱 ; 而計算機圖學領域的人可能會認為你正在撰寫關於三角形斜邊(hypotenuse)的程式;遊戲開發的人又會認為你在定義生命值,看似一個好的縮寫名稱,卻有可能造成極大的誤會,進而導致更大的麻煩。

UNIX 是一個 1960 年代開發出來的作業系統,相較於 Windows 與 macOS 等耳熟能詳的作業系統,UNIX 的優勢在於發展至今已經非常完善穩定,且具有完善的廠商售後技術服務支持,不過我本人並沒有使用過 UNIX 所以無法確切介紹給讀者。

在現今的開發環境裡,我們喜愛利用程式碼自動補齊的功能,若你的程式碼中每個命名都有極大的差異,且每個名稱都極大化的代表該變數、方法的作用,這便能大大的提升我們的編碼效率,因此我想再次強調,好的命名會帶給你的意想不到的好處。

自動補齊功能:只需寫下名稱的少部分字母,按下某些快捷鍵組合,就會出現一連串可補齊的名稱。

有意義的區別

增加數字的序列或無意義的字詞或許可以滿足編譯器的規定「你無法在同一個視野範圍內,使用相同的名稱代表不同的東西。」,但這並不是一個好的命名方式,假如名稱必須有所不同,那麼它們也應該代表著不同的意義才是。以下程式為例:

public static void copyChars(char a1[], char a2[]) {
for (int i = 0; i<a1.length; i++) {
a2[i] = a1[i];
}
}

試想,如果我們將 👆 a2a1 改為用 👇 destinationsource 作為參數名稱是不是清楚了許多呢?我想這個例子也再一次證明,一時的懶惰,只會為往後帶來更大的麻煩。

public static void copyChars(char source[], char destination[]) {
for (int i = 0; i < source.length; i++) {
destination[i] = source[i];
}
}

每個概念使用一種字詞

接著,除了上述介紹的常見「偷懶」方法,我們還會犯一個我們自己都不會發現的錯,那就是對相同抽象概念或動作使用不同的字詞。

舉例來說,在命名「取得方法」時,採用了 fetchretrieveget 這些不同的名稱,就是一件經常發生的事情,也許你自己用習慣某個字詞沒什麼感覺,但當與他人協作開發時,你常需要記住是哪一家公司 retrieve、哪一個團隊使用 get,或是有些團隊根本沒有注意這件事,這會導致你必須花費大量時間,來查看與檢查程式碼,才能找到正確的用法。

加入有意義的上下文資訊

最後,我必須告訴你的是除了極少部份的命名能夠從命名本身了解到足夠意義,大部分命名還是需要上下文作為輔助,亦即將以上良好的命名放在擁有良好名稱的類別、函式或命名空間中,閱讀程式者才能夠完整的理解該名稱的意義。

以下我將提供一個處於含糊不清的上下文資訊的例子,這是一個列印統計數字的方法, pluralModifier 是複數型態修改器,希望讀者能夠先自己找找看其中命名有問題或是覺得不夠好的部分,我會在本文的留言區留下我認為的答案,有什麼想法都歡迎交流來共同提升!

private void printGuessStatistics (char candidate, int count) {
String number;
String verb;
String pluralModifier;
if (count == 0) {
number = "no";
verb = "are";
pluralModifier = "s";
} else if (count == 1) {
number = 1;
verb = "is";
pluralModifier = "";
} else {
number = Integer.toString (count);
verb = "are";
pluralModifier = "s";
}
String guessMessage = String.format (
"There %s %s %s%s", verb, number, candidate, pluralModifier
);
print (guessMessage);
}

結論

本系列進入第二篇,在 程式設計寶典(ㄧ)- 無瑕程式碼 中,我們介紹了本系列會詳細帶讀者學習的部分,而此篇是由命名為主題,希望能帶讀者通過具體的例子,以易懂的方式學習程式設計中的命名,我相信這並不難學,但卻能解決掉許多程式設計師經常面臨的問題 — 「粗心」而導致花了非常多時間在解決一些小問題上。

希望本篇的內容對你有幫助,我會持續的更新本系列文章,想把程式基礎打好的人,請跟隨我的腳步,花個 10 分鐘左右的時間,仔細斟酌、思考與比對自己的命名是否犯了以上我提及的問題,這會對讀者有很大的幫助,如果有任何問題歡迎於留言區留言!

參考資料

《Clean Code 無瑕的程式碼》

C# — var 隱含型別(Implicitly Typed) vs 顯式型別(Explicitly Typed)

Unix Introduction

代码洁癖系列(二):命名的艺术

UNIX 維基百科

Windows、Unix 和 Linux 优势劣势对比

Meaningful Names — A Dimension of writing Clean Code.

--

--

Rogerh.eth
誌瓜筆記

Sharing what i have learned for becoming a great developer.