本文列舉了我在周圍同事的Java程式碼中看到的一些比較典型的錯誤。顯然,靜態程式碼分析(我們團隊用的是qulice)不可能發現所有的問題,這也是為什麼我要在這裡列出它們的原因。
如果你覺得少了什麼,請不吝賜教,我會很樂意把它們加上去。
下面列出的所有這些錯誤基本上都與物件導向程式設計有關,尤其是Java的OOP。
類別名
讀下這篇短文「什麼是物件」。類別應該是真實生活中的一個抽象實體,而不是什麼“validators”,“controller”, “managers”這些東西。如果你的類別名稱以」er」結尾的話――那它就是個糟糕的設計。
當然了,工具類別也是反模式,比如說Apache的StringUtils, FileUtils, 以及IOUtils。上面這些都是糟糕設計的代表。延伸閱讀:OOP中工具類的替代方案。
當然,不要使用前綴或後綴來區分類和介面。比方說,這些名字就是錯誤的:IRecord, IfaceEmployee, 或RecordInterface。通常來說,介面名稱應該是真實生活中的實體的名字,類別名稱應該可以說明它的實作細節。如果這個實作沒有什麼特別可說明的,可以把它叫作Default, Simple或類似的什麼。比如說:
複製代碼代碼如下:
class SimpleUser implements User {};
class DefaultRecord implements Record {};
class Suffixed implements Name {};
class Validated implements Content {};
方法名
方法可以傳回值也可以回傳void。如果方法傳回值的話,它的名字應該能說明它回傳了什麼,比如說(永遠不要使用get前綴):
複製代碼代碼如下:
boolean isValid(String name);
String content();
int ageOf(File file);
如果它返回void,那麼它的名字應該要說明它做了什麼。比如:
複製代碼代碼如下:
void save(File file);
void process(Work work);
void append(File file, String line);
剛才提到的這些規則只有一個例外――JUnit的test方法不算。下面將會說到這個。
test方法的名字
在JUnit的測試案例中,方法名稱應該是沒有空格的英文語句。用一個例子來說明會更清楚一些:
複製代碼代碼如下:
/**
* HttpRequest can return its content in Unicode.
* @throws Exception If test fails
*/
public void returnsItsContentInUnicode() throws Exception {
}
你的JavaDoc裡的第一句話的開頭應該是你要測試的那個類別的名字,然後是一個can。因此,你的第一句話應該是類似「somebody can do something」。
方法名也是一樣的,只是沒有主題而已。如果我在方法名稱中間加一個主題的話,我就能得到一個完整的句子,正如上面那個例子中那樣:「HttpRequest returns its content in unicode」。
請注意test方法的名字是不以can開頭的。只有JavaDoc裡的的註解會以can開頭。除此之外,方法名不應該以動詞開頭。
實務上最好將測試方法宣告為拋出Exception的。
變數名
避免組合的變數名,比如說timeOfDay, firstItem,或是httpRequest。類別變數及方法內的變數都是如此。變數名稱應該夠長,避免在它的可見作用域內產生歧義,但是如果可以的話也不要太長。名字應該是單數或複數形式的名詞,或是適當的縮寫。比如:
複製代碼代碼如下:
List<String> names;
void sendThroughProxy(File file, Protocol proto);
private File content;
public HttpRequest request;
有的時候,如果建構方法要將入參保存到一個新初始化的物件中的時候,它的參數和類別屬性的名字可能會衝突。這種情況,我建議是去掉元音,使用縮寫。
範例:
複製代碼代碼如下:
public class Message {
private String recipient;
public Message(String rcpt) {
this.recipient = rcpt;
}
}
很多時候,看一下變數的類別名稱就知道變數該取什麼名字了。就用它的小寫形式就好了,像這樣就很可靠:
複製代碼代碼如下:
File file;
User user;
Branch branch;
然而,基礎類型的話,永遠不要這麼做,例如Integer number或String string。
如果存在多個不同性質的變數的話,可以考慮使用形容詞。比如:
複製代碼代碼如下:
String contact(String left, String right);
構造方法
不考慮異常的話,應該只有一個建構方法用來將資料儲存到物件變數中。其它構造方法則使用不同的參數來呼叫這個構造方法。比如說:
複製代碼代碼如下:
public class Server {
private String address;
public Server(String uri) {
this.address = uri;
}
public Server(URI uri) {
this(uri.toString());
}
}
一次性變數
無論如何都應該避免使用一次性變數。這裡我所說的「一次性「指的是只使用一次的變數。比如下面這個:
複製代碼代碼如下:
String name = "data.txt";
return new File(name);
上述的變數只會使用一次,因此這段程式碼可以重構成這樣:
複製代碼代碼如下:
return new File("data.txt");
有的時候,比較罕見的情況中――主要是為了格式更好看些――可能會用到一次性變數。然而,還是應盡量避免這種情況。
例外
毋庸贅言,永遠不要自己吞掉異常,而是應該當它盡量往上傳遞。私有方法應該始終把受檢查異常往外面拋。
不要使用異常來進行流程控制。比方說下面這段程式碼就是錯的:
複製代碼代碼如下:
int size;
try {
size = this.fileSize();
} catch (IOException ex) {
size = 0;
}
那如果IOException提示「磁碟已滿」的話該怎麼辦?你還會認為這個檔案大小為0,然後繼續往下處理?
縮排
關於縮進,主要的規則就是左括號要么在該行的末尾,要么就在同一行上閉合(對於右括號來說則相反)。比如說,下面這個就不正確,因為第一個左括號沒有在同一行上閉合,而它後面還有別的字元。第二個括號也有問題,因為它前面有字符,但對應的開括號又沒在同一行上:
複製代碼代碼如下:
final File file = new File(directory,
"file.txt");
正確的縮排應該是這樣的:
複製代碼代碼如下:
StringUtils.join(
Arrays.asList(
"first line",
"second line",
StringUtils.join(
Arrays.asList("a", "b")
)
),
"separator"
);
關於縮進,第二條重要的規則就是同時一行中應該盡量多寫一些――上限是80個字元。上面的例子並不滿足這一點,它還可以收縮一下:
複製代碼代碼如下:
StringUtils.join(
Arrays.asList(
"first line", "second line",
StringUtils.join(Arrays.asList("a", "b"))
),
"separator"
);
多餘的常量
當你希望在類別的方法中分享資訊的時候,應使用類別常數,這些資訊應該是你這個類別所特有的。不要把常數當作字串或數值字面量的替代品來使用――這是非常糟糕的實踐方式,它會對程式碼造成污染。常量(正如OOP中的任何物件一樣)應在真實世界中有它自己的意義。看下這些常量在真實生活中的意思是什麼:
複製代碼代碼如下:
class Document {
private static final String D_LETTER = "D"; // bad practice
private static final String EXTENSION = ".doc"; // good practice
}
另一個常見的錯誤就是在單元測試中使用常數來避免測試方法中出現冗餘的字串或數值的字面量。不要這麼做!每個測試方法都應該有自己專屬的輸入值。
在每個新的測試方法中使用新的文字或數值。它們是相互獨立的。那為什麼它們還要共享同樣的輸入常數呢?
測試數據耦合
以下是測試方法中資料耦合的一個例子:
複製代碼代碼如下:
User user = new User("Jeff");
// maybe some other code here
MatcherAssert.assertThat(user.name(), Matchers.equalTo("Jeff"));
在最後一行中,”Jeff”和第一行中的同一個字串字面值發生了耦合。如果過了幾個月,有人想把第三行這個值換一下,那麼他還得花時間找出同一個方法中哪裡也使用了這個」Jeff」。
為了避免這種情況,你最好還是引入一個變數。