程式碼規範是程式設計師的基本功,也是團隊協作的基石。本章涵蓋命名、組織、註釋、聲明等核心主題。
GoToFail 漏洞的教訓#
2014 年,蘋果公司的 SSL/TLS 實作中出現了一個嚴重的安全漏洞,根本原因竟是一個簡單的編碼風格問題:
// 有漏洞的程式碼
if ((error = doSomething()) != 0)
goto fail;
goto fail; // 這行總是執行!
if ((error = doMore()) != 0)
goto fail;第二個
goto fail不在 if 條件內,導致後續驗證被跳過,任何憑證都會被接受。
如果遵循「總是使用大括號」的規範,問題一目了然:
if ((error = doSomething()) != 0) {
goto fail;
goto fail; // 顯然多餘
}教訓:編碼規範不是繁文縟節,而是防止低級錯誤的防線。
命名原則#
命名的重要性#
「計算機科學只有兩件難事:快取失效和命名。」—— Phil Karlton
好的命名能讓程式碼自我解釋,減少註釋需求。
命名方法#
| 方法 | 格式 | 適用場景 |
|---|---|---|
| 大駝峰 | InputStream | Java 類別、介面 |
| 小駝峰 | firstName | Java 方法、變數 |
| 蛇形 | max_value | C 語言、常數 |
| 串式 | background-color | CSS 屬性 |
Java 命名規範#
// 類別:大駝峰
public class UserService { }
// 方法:小駝峰,動詞開頭
public void calculateTotal() { }
// 變數:小駝峰,名詞
String firstName;
int itemCount;
// 常數:全大寫蛇形
static final int MAX_RETRY_COUNT = 3;命名三原則#
準確 - 名副其實,見名知意
// 差 int d; // elapsed time in days // 好 int elapsedTimeInDays;規範 - 遵循語言慣例
// 差:混用命名風格 String user_name; // 蛇形 String firstName; // 駝峰 // 好:統一使用駝峰 String userName; String firstName;可讀 - 寧長勿短
// 差 List<String> lst; // 好 List<String> userNames;
取名要做到「信、達、雅」—— 準確、直觀、優美。
程式碼組織#
分塊思維#
大腦按「塊」處理資訊。好的程式碼組織讓每個邏輯塊清晰可辨:
// 差:擠在一起
if(firstName!=null&&lastName!=null)
// 好:清晰分塊
if ((firstName != null) && (lastName != null))空白的運用#
| 元素 | 用途 | 範例 |
|---|---|---|
| 空格 | 同行內區隔 | a + b |
| 縮排 | 層級關係 | 4 空格或 2 空格 |
| 空行 | 區塊分隔 | 方法之間 |
一行一行為#
// 差:兩個行為在一行
if (variable != null) variable.doSomething();
// 好:分開兩行
if (variable != null) {
variable.doSomething();
}換行原則#
- 每行不超過 80-120 字元
- 在逗號後換行
- 在運算符前換行
- 新行與上一行同級表達式對齊
String result = calculateSomething(longParameterOne,
longParameterTwo, longParameterThree);
String value = longStringOne + longStringTwo
+ longStringThree;註釋的藝術#
註釋的三個麻煩#
- 難以維護 - 註釋不會編譯,容易過時
- 成為藉口 - 過度依賴註釋,忽略程式碼本身的可讀性
- 容易濫用 - 被用來注釋掉程式碼或寫俏皮話
最好的程式碼不需要註釋。如果需要註釋,說明程式碼還不夠清晰。
三種註釋風格#
1. 版權聲明
/*
* Copyright (c) 2024, Company Name. All rights reserved.
*/2. API 檔案(Javadoc)
/**
* 計算兩數之和。
*
* @param a 第一個加數
* @param b 第二個加數
* @return 兩數之和
* @throws IllegalArgumentException 如果參數為負數
*/
public int add(int a, int b) { }3. 實作說明
// 使用二分搜尋提高效能
int index = Collections.binarySearch(list, target);註釋三原則#
| 原則 | 說明 |
|---|---|
| 準確 | 錯誤的註釋比沒有更糟 |
| 必要 | 多餘的註釋浪費閱讀時間 |
| 清晰 | 混亂的註釋讓程式碼更亂 |
不要在程式碼裡保留 TODO、除錯語句或注釋掉的程式碼。
聲明規範#
八項紀律#
取好名字 - 遵循命名規範
一行一個聲明 - 便於維護
// 差 int width, height; // 好 int width; int height;需要時再聲明 - 區域變數靠近使用處
類屬性集中聲明 - 便於查找
聲明時初始化 - 防止遺漏
左括號不單獨成行 - Java 慣例
public void method() { // 好 // ... }小括號緊靠標識符 -
method()而非method ()搜尋最佳化的換行 - 常見搜尋模式放同一行
類的內部結構#
推薦順序:
- 類屬性
- 建構子
- 工廠方法
- 公開方法
- 私有方法
控制流與條件式#
深層嵌套是可讀性殺手#
嵌套每深一層,讀者就要多扛一個「目前在哪個條件下」的心智狀態。超過 3 層,理解成本急遽上升。
| 嵌套深度 | 心智負荷 | 建議 |
|---|---|---|
| 1-2 層 | 低 | 一般可接受 |
| 3 層 | 中 | 開始警惕,考慮攤平 |
| > 3 層 | 高 | 幾乎都該重構 |
攤平手段:guard clause(衛述句)與早返回。 把例外/邊界情況在開頭擋掉,主流程就能保持在最外層。
// 壞味道:深層嵌套,主邏輯被埋在最內層
public double payAmount(Employee employee) {
double result;
if (employee.isSeparated()) {
result = separatedAmount();
} else {
if (employee.isRetired()) {
result = retiredAmount();
} else {
result = normalPayAmount();
}
}
return result;
}
// 改善:guard clause 早返回,主流程攤平
public double payAmount(Employee employee) {
if (employee.isSeparated()) {
return separatedAmount();
}
if (employee.isRetired()) {
return retiredAmount();
}
return normalPayAmount();
}以多型或表驅動取代冗長條件#
長串 if-else 或 switch,尤其是同一組判斷散落多處時,是引入多型或查表的信號。
// 壞味道:switch 隨型別擴張而膨脹
String icon;
switch (animal.getType()) {
case DOG: icon = "dog.png"; break;
case CAT: icon = "cat.png"; break;
case BIRD: icon = "bird.png"; break;
default: icon = "unknown.png";
}
// 改善 A:表驅動(型別與資料對應時最簡潔)
private static final Map<AnimalType, String> ICONS = Map.of(
AnimalType.DOG, "dog.png",
AnimalType.CAT, "cat.png",
AnimalType.BIRD, "bird.png");
String icon = ICONS.getOrDefault(animal.getType(), "unknown.png");
// 改善 B:多型(型別還帶有不同「行為」時更適合)
abstract class Animal {
abstract String icon();
}純粹「資料對應」用表驅動(Map)最輕;若每個分支還帶不同行為,才升級為多型。先選最輕的方案。
魔術數字改用命名常數#
字面量散在邏輯裡,讀者無從得知它的意義,改值時還可能漏改。
// 壞味道:3 是什麼?86400 又是什麼?
if (retryCount > 3) { ... }
long ttl = id * 86400;
// 改善:命名常數自我解釋
private static final int MAX_RETRY = 3;
private static final int SECONDS_PER_DAY = 86_400;
if (retryCount > MAX_RETRY) { ... }
long ttl = id * SECONDS_PER_DAY;函式簽名與參數設計#
參數過多的信號與參數物件#
參數一多(經驗上 >3-4 個就要警惕),呼叫端容易傳錯順序、簽名也難讀。當一組參數總是「一起出現」,它們往往是一個尚未命名的概念——抽成 Parameter Object。
// 壞味道:參數過多,且 start/end 總是成對出現
List<Reading> readingsInRange(Meter meter, LocalDate start,
LocalDate end, boolean includeEstimated);
// 改善:把成對概念抽成參數物件
record DateRange(LocalDate start, LocalDate end) {
DateRange {
if (end.isBefore(start)) {
throw new IllegalArgumentException("end before start");
}
}
}
List<Reading> readingsInRange(Meter meter, DateRange range,
boolean includeEstimated);參數物件不只縮短簽名,還給了驗證與相關行為一個歸宿(如上例 DateRange 自帶不變量檢查)。
旗標參數是拆分的信號#
布林旗標讓一個函式內部其實做了「兩件事」,呼叫端 doIt(true) 也無從得知 true 代表什麼。
// 壞味道:旗標參數
public void book(Concert concert, boolean isPremium) {
if (isPremium) { ... } else { ... }
}
booking.book(concert, true); // true 是什麼意思?
// 改善:拆成兩個意圖明確的函式
public void bookRegular(Concert concert) { ... }
public void bookPremium(Concert concert) { ... }例外:當旗標只是單純轉發給底層 API、且拆分會造成大量重複時,可保留。判準是「旗標是否改變了函式的核心行為」——若是,就該拆。
文件化前置/後置條件與不變量#
清楚標示「呼叫者要保證什麼(precondition)」「函式回傳時保證什麼(postcondition)」「物件始終維持什麼(invariant)」,能釐清責任邊界、減少防禦層數。
/**
* 從帳戶提款。
*
* @param amount 提款金額,必須為正數(precondition)
* @return 提款後的餘額,保證 >= 0(postcondition)
* @throws InsufficientBalanceException 餘額不足時
*/
public BigDecimal withdraw(BigDecimal amount) {
// 前置條件:對外部呼叫快速失敗
if (amount.signum() <= 0) {
throw new IllegalArgumentException("amount must be positive");
}
if (amount.compareTo(balance) > 0) {
throw new InsufficientBalanceException();
}
balance = balance.subtract(amount);
assert balance.signum() >= 0 : "invariant: balance never negative";
return balance;
}契約思維與防禦分層的完整討論,見 程式碼安全 的「合約式設計與進攻式編程」。
Java 註解的使用#
@Override#
覆寫的方法必須加上
@Override註解。
好處:
- 明確標示這是覆寫方法
- 父類修改時編譯器會報錯
class Student extends Person {
@Override
public String getFirstName() {
return super.getFirstName();
}
}@Deprecated#
及時廢棄不合理的 API:
/**
* @deprecated 使用 {@link #newMethod()} 代替
*/
@Deprecated(since = "2.0", forRemoval = true)
public void oldMethod() { }@SuppressWarnings#
盡量不要使用
@SuppressWarnings。警告是有價值的資訊。
例外處理#
例外分類#
| 類型 | 範例 | 處理方式 |
|---|---|---|
| Error | OutOfMemoryError | 不需處理 |
| RuntimeException | NullPointerException | 需記錄在文件 |
| Checked Exception | IOException | 必須宣告或捕獲 |
例外處理原則#
不要用例外處理正常流程
// 差 try { checkUserName(name); } catch (InvalidNameException e) { // 處理無效名稱 } // 好 if (isValidUserName(name)) { // 處理有效名稱 } else { // 處理無效名稱 }選擇精確的例外類型 - 不要用
Exception提供清晰的例外描述
throw new IllegalArgumentException( "User ID must be positive, but was: " + userId);
檔案組織#
專案結構#
project/
├── README # 軟體說明
├── COPYRIGHT # 版權資訊
├── LICENSE # 授權條款
├── src/ # 原始碼
│ └── com/example/
├── test/ # 測試程式碼
│ └── com/example/
└── docs/ # 文件原始碼檔案結構#
- 版權聲明
- package 宣告
- import 語句
- 類別 Javadoc
- 類別宣告
- 類別實作
程式碼規範檢查清單
命名
- 類別使用大駝峰
- 方法和變數使用小駝峰
- 常數使用全大寫蛇形
- 名稱準確表達意圖
格式
- 統一縮排風格
- 每行不超過 120 字元
- if/for/while 總是使用大括號
- 適當使用空行分隔邏輯塊
註釋
- 公開 API 有 Javadoc
- 沒有過時的註釋
- 沒有注釋掉的程式碼
聲明
- 一行一個聲明
- 變數靠近使用處聲明
- 覆寫方法有 @Override
例外
- 檔案記錄所有可能的例外
- 不用例外處理正常流程
- 例外訊息清晰明確