程式碼只需編寫一次,即可被閱讀數百次。編寫函數的開發者很少會在六個月後親自調試它、根據新需求擴展它,或在生產事故中理解它的極端情況。編寫程式碼時所做的每一個決定,例如變數名稱、函數長度、類別結構等等,都會影響後續讀者的工作,或輕鬆或困難。編寫整潔程式碼的精髓就在於系統性地簡化後續閱讀流程。
羅伯特·C·馬丁(人稱「鮑伯大叔」)在其著作《代碼整潔之道:敏捷軟體開發手冊》(2008)中正式闡述了這一概念,該書至今仍是該領域的權威參考書。馬丁‧福勒的《重構:改進現有程式碼的設計》則探討了與之互補的問題:當現有程式碼不夠整潔且需要改進時,應該如何處理。這兩部著作共同奠定了程式碼整潔實踐的理論基礎。本指南涵蓋了它們的關鍵原則,並提供了涵蓋最常用語言(包括RPG和PL/I)的實用程式碼範例,適用於維護企業遺留系統的開發人員。
什麼是整潔程式碼?
整潔的程式碼是指易於閱讀、易於理解、易於測試和易於修改的原始程式碼。這裡的「易於」指的是實際操作層面的易於操作。編譯器能夠接受的程式碼不一定就是整潔的程式碼。通過所有測試的程式碼也不一定就是整潔的程式碼。當一段程式碼在編寫時未預料到的情況下,由其他開發者(而非程式碼的編寫者)能夠快速理解其意圖,自信地進行修改,並進行擴展而不會產生意想不到的後果時,這段程式碼才是整潔的。
馬丁·福勒的定義被引用最多:「任何傻瓜都能寫出電腦能理解的程式碼。優秀的程式設計師寫出的程式碼,人類也能理解。」衡量程式碼是否優秀的標準是人類的理解能力,而不是機器的執行能力。
整齊的程式碼並非關乎美觀,也並非為了遵循某種特定的風格指南而遵循。它關乎程式碼的結構特性,這些特性決定了未來每次修改所需的工作量。違反整潔程式碼原則的程式碼庫會不斷累積技術債務,這是過去偷工減料所帶來的累積成本,最終導致每個新功能都需要先理解並仔細梳理現有程式碼的複雜性,才能添加新的內容。
程式碼整潔原則:快速參考
Robert C. Martin 的程式碼整潔原則可以概括為一系列可操作的指導原則。下表將每項原則與其核心規則以及它所預防的問題對應起來:
| 原則 | 核心規則 | 它能預防的問題 |
|---|---|---|
| 有意義的名字 | 名稱應體現意圖、變數、函數和類 | 認知開銷解碼什麼 x, tmp, 或者 obj 實際上代表 |
| 小功能 | 函數只做一件事;適合顯示在一個螢幕上 | 無法測試、難以閱讀的單體架構,職責混雜。 |
| 單一職責 | 每個課程/模組都有一個需要更改的原因。 | 神級職業會在十幾個互不相關因素中的任何一個發生變化時失效。 |
| DRY(不要重複自己) | 每一種知識都有其唯一的表示形式。 | 錯誤修復僅應用於一個副本,而未應用於其他副本;邏輯偏移 |
| KISS(保持簡單) | 優先選擇最簡單有效的解決方案。 | 過度設計的程式碼,解決的卻是沒人需要的問題。 |
| YAGNI(你不需要它) | 只有在需要的時候才會開發功能。 | 毫無意義的死程式碼,只會增加複雜性而沒有任何好處 |
| 開放/封閉原則 | 開放延期,關閉修改 | 新增行為時,會破壞現有行為的程式碼 |
| 關注點分離 | 不同地方的職責不同 | 程式碼錯綜複雜,改動其中一點會導致不相關的問題。 |
| 避免評論“是什麼”,而要評論“為什麼”。 | 代碼解釋了什麼;註釋解釋了為什麼。 | 過時的評論,誤導性大於資訊價值。 |
| 童子軍規則 | 讓程式碼比你最初發現它時更簡潔。 | 品質因逐漸忽略而逐漸下降 |
核心程式碼整潔原則詳解
乾脆,不要重複自己
DRY(Don't Repeat Yourself,不要重複自己)原則是指系統中的每一項知識都必須有單一、明確且權威的表示形式。當相同的邏輯出現在多個地方時,這些表示形式必然會不一致。業務規則的變更就需要跨多個文件進行查找和替換,而遺漏任何副本都會導致錯誤。
DRY 原則不僅適用於複製貼上的程式碼,也適用於設定、文件和資料模式。如果相同的資訊需要在兩個地方維護,無論是否直接複製了任何程式碼,都違反了 DRY 原則。
KISS,保持簡單
KISS原則認為,系統越簡單,運作效果越好,簡潔性應該是首要的設計目標。複雜性並非精妙的標誌,而是顯示某些內容本來可以表達得更清晰。
實際意義在於:當兩種解決方案都能解決同一個問題時,優先選擇更簡單的方案。更複雜的方案或許可以處理你尚未遇到的極端情況,但肯定會讓所有接觸到它的人更難理解程式碼。
YAGNI,你不需要它
YAGNI(You Agents I GNI,你不需要它)原則不鼓勵在真正需要之前就加入功能。它源自於一種常見的衝動:為了因應預期的未來需求而建立靈活、可擴展的系統,而這些需求最終往往不會實現。每一個為假想的未來用例而存在的抽象、介面和配置選項,都會給今天閱讀程式碼的開發者帶來認知負擔。
有意義的名字
名稱是原始碼中的主要通訊機制。一個名為 `name` 的函數。 process() 它不提供任何關於它處理什麼、何時處理或返回什麼的資訊。一個名為 calculateMonthlyInterest() 溝通精準。
判斷一個名稱好壞的關鍵在於:不看實現程式碼,你能明白它代表什麼嗎?名稱是否揭示了它的用途、參數和返回值?是否需要註釋來解釋它的作用?
小功能
羅伯特·C·馬丁的經驗法則是:函數應該小,比你認為必要的還要小。一個只做一件事的函數可以命名清晰,可以單獨測試,而且無需閱讀實作就能理解。而一個做多件事的函數則需要同時理解所有功能。
單一職責原則也適用於函數:一個函數應該只做一件事,把它做好,並且只做這件事。如果需要用「和」這個字來描述一個函數的功能,那麼它可能承擔了太多功能。
Java 中的整潔程式碼
Java 程式碼冗長,因此編寫簡潔的程式碼尤其重要。如果管理不當,該語言要求的樣板代碼可能會掩蓋代碼的意圖。
Java的
// Before: unclear names, mixed responsibilities, magic numbers
public double calc(int x, int y) {
double r = 0;
if (y > 1000) {
r = x * y * 0.1;
} else {
r = x * y * 0.05;
}
return r;
}
// After: meaningful names, single responsibility, named constants
private static final double PREMIUM_DISCOUNT_RATE = 0.10;
private static final double STANDARD_DISCOUNT_RATE = 0.05;
private static final int PREMIUM_THRESHOLD = 1000;
public double calculateDiscount(int quantity, int unitPrice) {
double subtotal = quantity * unitPrice;
return isPremiumOrder(unitPrice)
? subtotal * PREMIUM_DISCOUNT_RATE
: subtotal * STANDARD_DISCOUNT_RATE;
}
private boolean isPremiumOrder(int unitPrice) {
return unitPrice > PREMIUM_THRESHOLD;
}
Java 程式碼整潔的關鍵實踐包括:優先使用組合而非繼承,使用流進行資料處理而非冗長的循環,將「魔法數字」提取為命名常數,以及保持類別只負責單一職責。 Robert C. Martin 的 Java 程式碼整潔原則之一是,類別應該小,衡量標準不是程式碼行數,而是職責。
Java的
// Clean Java: streams over imperative loops
List<String> activeUserEmails = users.stream()
.filter(User::isActive)
.map(User::getEmail)
.collect(Collectors.toList());
Python 中的整潔程式碼
Python 的設計理念-顯式優於隱式,簡潔優於複雜-與整齊程式碼的原則天然契合。 PEP 8 是 Python 的官方風格指南,也是編寫整潔 Python 程式碼的基準。
蟒蛇
# Before: vague names, long function, no separation
def do_stuff(d):
res = []
for i in d:
if i['a'] > 18:
res.append(i['n'].upper())
return res
# After: meaningful names, separated concerns, Pythonic style
def get_adult_names_uppercase(users: list[dict]) -> list[str]:
return [
user["name"].upper()
for user in users
if user["age"] > 18
]
蟒蛇
# Clean Python: context managers for resource handling
# Before: manual, error-prone
f = open("data.txt")
data = f.read()
f.close()
# After: guaranteed cleanup, self-documenting intent
with open("data.txt") as f:
data = f.read()
Pythonic 的簡潔程式碼使用列表推導式而不是手動循環來進行簡單的轉換,使用類型提示來實現自文檔化的函數簽名,使用上下文管理器來進行資源管理,以及使用資料類或命名元組而不是裸字典來處理結構化資料。
JavaScript 和 TypeScript 中的整齊程式碼
JavaScript 的靈活性既是它的優勢,也是編寫簡潔程式碼的主要挑戰。缺乏規範,JavaScript 程式碼庫會累積不一致的模式、隱式類型強制轉換和混亂的回呼鏈。
JavaScript的
// Before: var, callback hell, no error handling
function getUser(id, cb) {
db.query('SELECT * FROM users WHERE id = ' + id, function(err, rows) {
if (err) cb(err);
cb(null, rows[0]);
});
}
// After: async/await, parameterized query, proper error handling
async function getUserById(userId: number): Promise<User | null> {
const [rows] = await db.execute(
'SELECT * FROM users WHERE id = ?',
[userId]
);
return rows[0] ?? null;
}
打字稿
// Clean TypeScript: explicit types replace implicit any
// Before
function process(data) {
return data.map(x => x.v * 2);
}
// After
interface DataPoint {
value: number;
label: string;
}
function doubleValues(dataPoints: DataPoint[]): number[] {
return dataPoints.map(point => point.value * 2);
}
簡潔的 JavaScript 使用 const 預設情況下, let 只有在需要重新綁定時才需要,而且絕對不需要 var純函數,相同的輸入總是產生相同的輸出,沒有副作用,是 JavaScript 邏輯最簡潔的建置模組。
C# 中的整潔程式碼
C# 提供了強大的功能,可用於編寫簡潔、富有表現力的程式碼。該語言的發展歷程,例如 LINQ、記錄、模式匹配和可空引用類型,始終朝著更具聲明性和可讀性的語法方向邁進。
尖銳的
// Before: magic numbers, verbose loop, mutable state
public double CalculateTotal(List<OrderItem> items)
{
double total = 0;
foreach (var item in items)
{
if (item.Quantity > 10)
total += item.UnitPrice * item.Quantity * 0.9;
else
total += item.UnitPrice * item.Quantity;
}
return total;
}
// After: named constant, LINQ, single expression
private const double BulkDiscountRate = 0.9;
private const int BulkDiscountThreshold = 10;
public double CalculateTotal(IEnumerable<OrderItem> items) =>
items.Sum(item => item.Quantity > BulkDiscountThreshold
? item.UnitPrice * item.Quantity * BulkDiscountRate
: item.UnitPrice * item.Quantity);
C# 程式碼整潔原則:使用屬性而不是公共欄位進行封裝,利用 LINQ 進行宣告式資料操作,使用記錄作為不可變資料結構,在方法簽章中優先使用介面而非具體型別,並使用可空引用型別(string? vs string)明確說明空安全性。
Kotlin 中的整潔程式碼
Kotlin 的設計旨在減少 Java 中難以保持程式碼整齊的樣板程式碼,同時添加特性、資料類別、擴展函數、空安全性等,從而自然地支援編寫整潔的程式碼。
科特林
// Before: verbose Java-style Kotlin
class User {
var name: String = ""
var email: String = ""
var age: Int = 0
}
fun processUsers(users: List<User>): List<String> {
val result = mutableListOf<String>()
for (user in users) {
if (user.age >= 18) {
result.add(user.email)
}
}
return result
}
// After: idiomatic clean Kotlin
data class User(val name: String, val email: String, val age: Int)
fun getAdultEmails(users: List<User>): List<String> =
users.filter { it.age >= 18 }.map { it.email }
Kotlin 的 data class 擴充函數會自動提供 equals、hashCode、copy 和 toString 方法,消除了導致 Java 資料類別冗長且容易出錯的樣板程式碼。擴充函數可讓您在不繼承的情況下為現有類別添加簡潔的實用方法。
RPG 和 PL/I 中的整潔程式碼:傳統語言,現代原則
程式碼整潔原則適用於所有程式語言,包括運行全球金融系統、保險平台和政府應用程式的企業級語言。 RPG(報表程式產生器)和PL/I在許多組織中仍然得到積極維護,而使Java或Python程式碼整潔的原則同樣也使RPG和PL/I程式碼得以持續發展。
RPG(ILE RPG)中的整潔程式碼:
籃板
// Before: cryptic two-character names, magic numbers
C EVAL D = Q * P * 1.05
C IF Q > 100
C EVAL D = Q * P * 0.95
C ENDIF
// After: meaningful names, named constants, clear intent
/free
dcl-c BULK_DISCOUNT_THRESHOLD 100;
dcl-c BULK_DISCOUNT_RATE 0.95;
dcl-c STANDARD_RATE 1.05;
dcl-proc CalculateOrderTotal;
dcl-pi *N packed(15:2);
quantity packed(7:0) value;
unitPrice packed(9:2) value;
end-pi;
if quantity > BULK_DISCOUNT_THRESHOLD;
return quantity * unitPrice * BULK_DISCOUNT_RATE;
else;
return quantity * unitPrice * STANDARD_RATE;
endif;
end-proc;
/end-free
關鍵的乾淨角色扮演遊戲實踐:使用 ILE RPG 的自由格式(/free為了提高可讀性,使用語法而非固定格式,對過程進行描述性命名,並用命名常數取代硬編碼值。 dcl-c並使用以下方法將長程序分解為聚焦過程 dcl-proc.
PL/I 中的簡潔程式碼:
PLI
/* Before: single-letter names, no structure */
CALC: PROC(X, Y) RETURNS(FLOAT);
DCL (X, Y, R) FLOAT;
IF Y > 1000 THEN R = X * Y * 0.1;
ELSE R = X * Y * 0.05;
RETURN(R);
END CALC;
/* After: meaningful names, named constants, clear intent */
DCL PREMIUM_THRESHOLD FIXED DECIMAL(7) INIT(1000);
DCL PREMIUM_RATE FLOAT INIT(0.10);
DCL STANDARD_RATE FLOAT INIT(0.05);
CALCULATE_DISCOUNT: PROC(QUANTITY, UNIT_PRICE) RETURNS(FLOAT);
DCL (QUANTITY, UNIT_PRICE) FLOAT;
DCL SUBTOTAL FLOAT;
SUBTOTAL = QUANTITY * UNIT_PRICE;
IF UNIT_PRICE > PREMIUM_THRESHOLD
THEN RETURN(SUBTOTAL * PREMIUM_RATE);
ELSE RETURN(SUBTOTAL * STANDARD_RATE);
END CALCULATE_DISCOUNT;
PL/I 的整潔程式碼原則與現代語言的原則類似:有意義的標識符(PL/I 的 31 個字元限制足以用於描述性名稱)、命名常數而不是魔法數字、專注於單一任務的過程以及使用 ON 條件系統進行明確錯誤處理。
程式碼整潔工具和靜態分析
透過程式碼審查手動應用整潔程式碼原則固然必要,但大規模應用時還不夠。靜態分析工具可以自動執行整潔程式碼指標,在程式碼合併前標記違規之處。
| 工具 | 語言 | 它強制執行的內容 |
|---|---|---|
| SonarQube / SonarCloud | 超過30種語言 | 複雜性、重複程式碼、程式碼異味、安全性 |
| 格子風格 | Java的 | 命名規則、格式、結構 |
| PMD | Java,Apex | 重複程式碼、未使用的變數、複雜性 |
| ESLint + typescript-eslint | JavaScript、打字稿 | 風格、複雜性、未使用的程式碼、非同步模式 |
| 皮林特 + 氡 | 蟒蛇 | PEP 8、複雜度、可維護性指數 |
| 銳化器 | C# | 程式碼風格、冗餘程式碼、重構建議 |
| 大眼夾 | 銹 | 習慣用語模式,常見錯誤 |
| SMART TS XL | COBOL、RPG、PL/I、Java、Python 等 | 跨語言複雜性、重複程式碼、死程式碼 |
工具測量的最常見程式碼品質指標有:圈複雜度(決策分支數,超過 10 個是警告,超過 20 個是嚴重問題)、認知複雜度(程式碼的理解難度,SonarQube 特有的指標,在可讀性測量方面優於圈複雜度)和重複率(重複程式碼的百分比,超過 3% 需要注意)。
SMART TS XL 在企業程式碼庫中強制執行程式碼整潔性
上述程式碼整潔工具均適用於單一語言。但在企業環境中,Java 服務、Python 管線、COBOL 批次程式、RPG 模組和 JCL 作業流程等多種語言並存,因此需要同時評估每種語言的程式碼整潔性違規情況,且跨語言元件之間的關係與單一檔案的程式碼品質同樣重要。
SMART TS XL“ 靜態程式碼分析 它同時對環境中所有語言應用整潔程式碼指標。對於 COBOL 程序,其圈複雜度、重複程式碼率、死程式碼識別和結構耦合度指標的計算方法與 Java 類別相同,從而在整個應用程式組合中產生可比較的統一品質測量結果。
影響分析功能使得在架構層面上對違反規範的程式碼進行處理成為可能:當一個 COBOL 程式碼段與數十個其他程式高度耦合時,影響分析可以在任何重構開始之前準確地顯示哪些元件受到影響。這使得團隊在大型程式碼庫中遇到的重構癱瘓問題轉變為具有明確範圍的結構化修復方案。
應用程式依賴關係映射可以識別系統層面的架構違規行為,例如:哪些元件在企業級規模下變成了「上帝類」(God Classes),哪些循環依賴違反了跨語言邊界的關注點分離原則,以及哪些相同的業務邏輯在多個系統中獨立實現,彼此互不感知。這種跨語言的 DRY(Don't Repeat Yourself,不要重複自己)違規行為——即同一業務規則在 COBOL 程式、Java 服務和 Python 管道中分別維護——是企業系統中代價最高的程式碼違規類型,而且任何單一語言工具都無法檢測到它。
對於在以下情況下應用整潔程式碼原則的團隊: 遺產現代化 程式, SMART TS XL 提供預重構分析,使工作變得可控:在轉換開始之前將死程式碼排除在範圍之外,確定複雜度最高的元件以便優先關注,並在進行任何更改之前提供完整的依賴關係圖。
編寫整潔程式碼是一種團隊協作,而非個人行為。
本指南中的原則說來容易做來難。任何開發者都能獨立編寫出簡潔的程式碼。真正的挑戰在於如何在團隊內部、隨著時間的推移以及程式碼庫不斷增長且貢獻者不斷更迭的情況下,保持程式碼的整潔。這需要三點:所有人都了解並認可的共享標準;能夠自動執行這些標準的工具;以及一種持續改進的文化,就像童子軍守則一樣,始終如一地貫徹執行,讓代碼的每一部分都比最初更簡潔。
羅伯特·C·馬丁的論述依然最為精闢:「簡潔的程式碼總是給人一種用心編寫的感覺。」用心編寫的程式碼並非體現在沒有 bug,而是體現在易讀的命名、專注的函數、清晰的結構以及避免意外情況。正是這些體現,使得程式碼庫值得投入、值得貢獻,也值得在任何成功的系統經歷多年的發展和團隊更迭後繼續維護。