程式碼品質不只是「能動就好」。資深工程師在審查程式碼時,會從可維護性、可測試性、效能、安全性等多個面向進行評估。本文將詳細解析資深工程師審查程式碼時關注的各個項目。
什麼是 Senior Engineering Review?
Senior Engineering Review 是一套系統化的程式碼審計流程,用於評估:
- 程式碼品質:可讀性、可維護性、一致性
- 架構設計:模組化、耦合度、擴展性
- 最佳實踐:錯誤處理、型別安全、文件化
- 效能考量:資源使用、查詢效率、快取策略
一、程式碼品質標準(Code Quality Standards)
1. 錯誤處理(Error Handling)
| 項目 | 說明 |
|---|
| 檢查內容 | 是否正確處理所有可能的錯誤情境 |
| 目的 | 避免程式崩潰、提供有意義的錯誤訊息 |
為什麼重要?
未處理的錯誤可能導致:
- 程式無預警崩潰
- 使用者體驗受損
- 安全漏洞(錯誤訊息洩露敏感資訊)
- 難以除錯的問題
最佳實踐
// ❌ 不好:忽略錯誤
try {
await saveData(data);
} catch (e) {
// 吞掉錯誤,什麼都不做
}
// ❌ 不好:通用錯誤處理
try {
await saveData(data);
} catch (e) {
console.log('Error'); // 沒有有用資訊
}
// ✅ 好:具體處理錯誤
try {
await saveData(data);
} catch (e) {
if (e instanceof ValidationError) {
return { error: 'Invalid data format', details: e.message };
}
if (e instanceof NetworkError) {
return { error: 'Network unavailable, please retry' };
}
// 記錄未預期的錯誤供日後分析
logger.error('Unexpected error in saveData', { error: e, data });
return { error: 'An unexpected error occurred' };
}
優缺點
| 優點 | 缺點 |
|---|
| 程式更穩定 | 需要較多程式碼 |
| 易於除錯 | 需要了解可能的錯誤類型 |
| 使用者體驗更好 | 可能過度複雜化 |
2. 型別安全(Type Safety)
| 項目 | 說明 |
|---|
| 檢查內容 | 是否使用嚴格的 TypeScript 型別 |
| 目的 | 在編譯時期捕捉錯誤,提升程式碼可靠性 |
常見問題
// ❌ 使用 any 型別
function processData(data: any): any {
return data.items.map((item: any) => item.value);
}
// ✅ 使用明確型別
interface Item {
id: string;
value: number;
}
interface Data {
items: Item[];
}
function processData(data: Data): number[] {
return data.items.map(item => item.value);
}
何時使用 any?
| 情境 | 建議 |
|---|
| 快速原型開發 | 可接受,但要標註 TODO |
| 第三方 API 回應 | 使用 unknown + type guard |
| 複雜泛型 | 考慮 as 斷言,但加註解 |
| 無法改變的舊程式碼 | 逐步遷移 |
優缺點
| 優點 | 缺點 |
|---|
| 編譯時發現錯誤 | 學習曲線 |
| IDE 自動補全 | 型別定義需要維護 |
| 程式碼自文件化 | 第三方庫可能缺少型別 |
3. 命名規範(Naming Conventions)
| 項目 | 說明 |
|---|
| 檢查內容 | 變數、函式、類別的命名是否描述性且一致 |
| 目的 | 提升程式碼可讀性 |
命名原則
// ❌ 不好的命名
const d = new Date();
const arr = users.filter(u => u.a);
function proc(x) { ... }
// ✅ 好的命名
const currentDate = new Date();
const activeUsers = users.filter(user => user.isActive);
function processUserRegistration(userData) { ... }
命名慣例
| 類型 | 慣例 | 範例 |
|---|
| 變數 | camelCase | userName, isActive |
| 函式 | camelCase, 動詞開頭 | getUserById, validateInput |
| 常數 | UPPER_SNAKE_CASE | MAX_RETRY_COUNT, API_BASE_URL |
| 類別 | PascalCase | UserService, HttpClient |
| 介面 | PascalCase | UserData, ApiResponse |
優缺點
| 優點 | 缺點 |
|---|
| 程式碼自解釋 | 名稱可能變長 |
| 減少註解需求 | 需要團隊一致性 |
| 易於搜尋 | 有時難以想到好名稱 |
4. 函式設計(Function Design)
| 項目 | 說明 |
|---|
| 檢查內容 | 函式是否保持小型且單一職責 |
| 目的 | 提升可測試性和可重用性 |
單一職責原則
// ❌ 做太多事的函式
async function handleUserRegistration(data) {
// 驗證
if (!data.email || !data.password) throw new Error('...');
if (data.password.length < 8) throw new Error('...');
// 儲存
const user = await db.users.create(data);
// 發送歡迎郵件
await sendEmail(user.email, 'Welcome!', welcomeTemplate);
// 記錄分析
await analytics.track('user_registered', { userId: user.id });
return user;
}
// ✅ 拆分成小函式
async function handleUserRegistration(data) {
validateRegistrationData(data);
const user = await createUser(data);
await sendWelcomeEmail(user);
await trackRegistration(user);
return user;
}
function validateRegistrationData(data) {
if (!data.email || !data.password) {
throw new ValidationError('Email and password are required');
}
if (data.password.length < 8) {
throw new ValidationError('Password must be at least 8 characters');
}
}
函式長度指引
| 指標 | 建議 |
|---|
| 行數 | < 30 行 |
| 參數數量 | ≤ 3 個(多了用 object) |
| 巢狀深度 | ≤ 3 層 |
| 認知複雜度 | 使用工具測量 |
| 項目 | 說明 |
|---|
| 檢查內容 | 註解是否有價值,而非重複程式碼 |
| 目的 | 解釋「為什麼」而非「做什麼」 |
好的註解 vs 壞的註解
// ❌ 重複程式碼的註解
// 設定使用者年齡為 18
user.age = 18;
// ❌ 過時的註解
// 回傳使用者名稱
function getUserEmail() { ... }
// ✅ 解釋業務邏輯
// 法規要求:未滿 18 歲不能註冊
if (user.age < 18) {
throw new Error('Must be 18 or older');
}
// ✅ 解釋技術決策
// 使用 setTimeout 而非 requestAnimationFrame
// 因為需要精確的時間控制,而非畫面更新同步
setTimeout(callback, 100);
何時需要註解
| 情境 | 需要註解 |
|---|
| 複雜演算法 | ✅ |
| 業務規則 | ✅ |
| 暫時解法(Workaround) | ✅ 加上 TODO |
| 非直覺的設計決策 | ✅ |
| 簡單的 CRUD 操作 | ❌ |
二、程式碼審查清單(Code Review Checklist)
1. 自我審查(Self-Review)
| 項目 | 說明 |
|---|
| 檢查內容 | 提交前是否自己先審查過 |
| 目的 | 減少明顯的錯誤,節省審查者時間 |
自我審查要點:
2. 測試涵蓋(Test Coverage)
| 項目 | 說明 |
|---|
| 檢查內容 | 新功能是否有對應的測試 |
| 目的 | 確保程式碼正確性,防止回歸 |
// 功能程式碼
function calculateDiscount(price: number, percentage: number): number {
if (percentage < 0 || percentage > 100) {
throw new Error('Invalid percentage');
}
return price * (1 - percentage / 100);
}
// 對應的測試
describe('calculateDiscount', () => {
it('should calculate 20% discount correctly', () => {
expect(calculateDiscount(100, 20)).toBe(80);
});
it('should throw for negative percentage', () => {
expect(() => calculateDiscount(100, -10)).toThrow();
});
it('should throw for percentage over 100', () => {
expect(() => calculateDiscount(100, 150)).toThrow();
});
});
3. 移除除錯程式碼
| 項目 | 說明 |
|---|
| 檢查內容 | 是否遺留 console.log、debugger 等 |
| 目的 | 保持程式碼整潔,避免效能影響 |
# 可以使用 git hooks 自動檢查
# .git/hooks/pre-commit
git diff --cached | grep -E "console\.(log|debug|info)" && \
echo "Error: console.log found" && exit 1
4. 安全考量
| 項目 | 說明 |
|---|
| 檢查內容 | 是否有硬編碼的密鑰、未驗證的輸入 |
| 目的 | 防止安全漏洞 |
// ❌ 硬編碼密鑰
const API_KEY = 'sk_live_REPLACEMENT_TOKEN';
// ✅ 使用環境變數
const API_KEY = process.env.API_KEY;
// ❌ 未驗證的輸入
const query = `SELECT * FROM users WHERE id = ${req.params.id}`;
// ✅ 參數化查詢
const query = 'SELECT * FROM users WHERE id = $1';
const result = await db.query(query, [req.params.id]);
三、架構決策(Architecture Decisions)
架構決策紀錄(ADR)
| 項目 | 說明 |
|---|
| 檢查內容 | 重大技術決策是否有文件記錄 |
| 目的 | 保留決策脈絡,方便未來參考 |
ADR 模板
## 決策:[決策標題]
### 背景
[為什麼需要做這個決策?]
### 考慮的選項
1. **選項 A** - [說明]
- 優點:...
- 缺點:...
2. **選項 B** - [說明]
- 優點:...
- 缺點:...
### 決策
[選擇了哪個選項,為什麼]
### 後果
[這個決策的影響和權衡]
1. 資料庫查詢
| 項目 | 說明 |
|---|
| 檢查內容 | 是否有 N+1 查詢問題 |
| 目的 | 避免效能瓶頸 |
// ❌ N+1 問題
const posts = await db.posts.findAll();
for (const post of posts) {
post.author = await db.users.findById(post.authorId); // N 次查詢
}
// ✅ JOIN 或預先載入
const posts = await db.posts.findAll({
include: [{ model: User, as: 'author' }]
});
2. 前端效能
| 項目 | 說明 |
|---|
| 檢查內容 | 是否考慮 bundle size、延遲載入 |
| 目的 | 提升使用者體驗 |
// ❌ 全部載入
import { everything } from 'huge-library';
// ✅ 按需載入
import { specificFunction } from 'huge-library/specificModule';
// ✅ 動態載入
const HeavyComponent = lazy(() => import('./HeavyComponent'));
3. 記憶體管理
| 項目 | 說明 |
|---|
| 檢查內容 | 是否有記憶體洩漏風險 |
| 目的 | 避免長時間運行的應用程式效能下降 |
// ❌ 忘記清理
useEffect(() => {
const interval = setInterval(updateData, 1000);
// 沒有 cleanup
}, []);
// ✅ 正確清理
useEffect(() => {
const interval = setInterval(updateData, 1000);
return () => clearInterval(interval);
}, []);
五、文件要求(Documentation Requirements)
何時需要更新文件
| 變更類型 | 需更新的文件 |
|---|
| 新功能 | README、CHANGELOG |
| API 變更 | API 文件、遷移指南 |
| 架構變更 | 架構文件、ADR |
| 設定變更 | 安裝指南、環境設定 |
JSDoc/TSDoc
/**
* 計算兩個日期之間的工作天數
*
* @param startDate - 開始日期
* @param endDate - 結束日期
* @param holidays - 國定假日列表
* @returns 工作天數
*
* @example
* ```ts
* const days = getWorkingDays(
* new Date('2026-01-01'),
* new Date('2026-01-31'),
* ['2026-01-01']
* );
* ```
*/
function getWorkingDays(
startDate: Date,
endDate: Date,
holidays: string[] = []
): number {
// ...
}
六、反模式(Anti-Patterns)
1. 過早優化
| 問題 | 說明 |
|---|
| 什麼是 | 在沒有效能資料的情況下進行優化 |
| 為什麼不好 | 浪費時間、增加複雜度 |
| 如何避免 | 先測量,再優化 |
// ❌ 過早優化:沒測量就假設這裡有效能問題
const cache = new Map();
function getData(id) {
if (cache.has(id)) return cache.get(id);
const data = fetchData(id);
cache.set(id, data);
return data;
}
// ✅ 先測量
// 發現 fetchData 確實在高流量時成為瓶頸後,再加入快取
2. 複製貼上程式設計
| 問題 | 說明 |
|---|
| 什麼是 | 複製相似程式碼而非抽取共用邏輯 |
| 為什麼不好 | 維護困難、bug 會在多處出現 |
| 如何避免 | 遵循 DRY 原則 |
3. 魔術數字/字串
| 問題 | 說明 |
|---|
| 什麼是 | 程式碼中的硬編碼數值 |
| 為什麼不好 | 意義不明、難以修改 |
| 如何避免 | 使用常數 |
// ❌ 魔術數字
if (user.age >= 18 && user.posts > 5) {
grantAccess();
}
// ✅ 有意義的常數
const MINIMUM_AGE = 18;
const MINIMUM_POSTS_FOR_ACCESS = 5;
if (user.age >= MINIMUM_AGE && user.posts > MINIMUM_POSTS_FOR_ACCESS) {
grantAccess();
}
4. 上帝類別(God Class)
| 問題 | 說明 |
|---|
| 什麼是 | 一個類別做太多事情 |
| 為什麼不好 | 難以理解、測試、維護 |
| 如何避免 | 單一職責原則,拆分成小模組 |
七、審查前檢查清單
程式碼品質
提交前
文件
總結
| 類別 | 重點項目 |
|---|
| 程式碼品質 | 錯誤處理、型別安全、命名、函式設計 |
| 審查流程 | 自我審查、測試、移除除錯碼、安全考量 |
| 架構 | ADR、模組化、職責分離 |
| 效能 | N+1 查詢、bundle size、記憶體管理 |
| 文件 | README、CHANGELOG、JSDoc |
| 反模式 | 過早優化、複製貼上、魔術數字、上帝類別 |
成為資深工程師不只是寫出「能動」的程式碼,更是寫出「可維護」、「可測試」、「可理解」的程式碼。持續進行程式碼審查和自我反思,是提升程式碼品質的關鍵。
延伸閱讀