
結論先講
同一個功能,不同工程師寫出來的 code 可讀性可以差十倍。Junior 跟 Senior 的差異,很大部分不是演算法或框架,是兩件事:
- 抽象化:看出多個具體問題的共同結構
- 命名:給事物取一個一看就懂的名字
這兩件事被低估是因為「看起來沒做什麼事」。但你讀別人的 code 覺得流暢跟覺得想吐,差別就在這。
這篇講怎麼練這兩個技能。不是背原則,是判斷力。
抽象化不是「少寫一點重複 code」
很多人對抽象化的誤解
Junior 看到三段類似 code 的反應:
function getUserEmail(id) {
const user = db.query('SELECT * FROM users WHERE id = ?', id);
return user.email;
}
function getUserPhone(id) {
const user = db.query('SELECT * FROM users WHERE id = ?', id);
return user.phone;
}
function getUserAddress(id) {
const user = db.query('SELECT * FROM users WHERE id = ?', id);
return user.address;
}「三段重複了」 → 抽象成:
function getUserField(id, field) {
const user = db.query('SELECT * FROM users WHERE id = ?', id);
return user[field];
}
getUserField(1, 'email');
getUserField(1, 'phone');這是壞的抽象。理由:
- 參數是字串
'email',TypeScript 檢查不到 typo - 沒表達意圖:
getUserField(1, 'address')讀不出來是要幹嘛 - 擴充困難:如果要組合多欄位?要格式化?要快取?
什麼是好的抽象
好的抽象看出共同結構,藏起不重要的細節,暴露語意。
function getUser(id) {
return db.query('SELECT * FROM users WHERE id = ?', id);
}
const email = getUser(id).email;
const phone = getUser(id).phone;這不是「少寫」,是正確切分邊界:
- 重複的部分:取 user → 抽成
getUser - 不重複的部分:取哪個欄位 → 交給使用者
還可以加快取:
const userCache = new Map();
function getUser(id) {
if (!userCache.has(id)) {
userCache.set(id, db.query('SELECT * FROM users WHERE id = ?', id));
}
return userCache.get(id);
}使用端不用改。這就是抽象的價值。
判斷「正確抽象層」的三個訊號
訊號 1:Rule of Three
看到第一次重複時,不要急著抽象。可能只是巧合。 看到第二次重複,仍然忍住。可能只是兩個不同的東西。 看到第三次重複,才抽象。這時你能看到「真正的共同點」是什麼。
早抽象的結果:抽錯。因為你沒看夠例子,不知道什麼是真共性、什麼是假巧合。
訊號 2:抽象名稱能不能一句話講完
// ❌ 名稱暴露實作細節
function processUserDataAndCheckPermissionsAndSendEmail() {}
// ❌ 名稱太泛
function handleStuff() {}
// ✅ 名稱 = 這個抽象做什麼
function notifyUserOfPasswordReset() {}如果你命名寫不出來 → 抽象沒想清楚。不要急著動手。
訊號 3:「這個抽象可以換掉底層嗎?」
好的抽象讓你能換實作:
interface MessageSender {
send(to: string, body: string): Promise<void>;
}
class EmailSender implements MessageSender { ... }
class SMSSender implements MessageSender { ... }
class SlackSender implements MessageSender { ... }壞的抽象綁死實作:
// 名稱綁死 email
class EmailService {
sendEmail(to: string, body: string) { ... }
}
// 要加 SMS 時,這個名字就尷尬了命名:Uncle Bob 的原則(精選版)
原則 1:名稱 = 意圖,不是實作
// ❌ 說怎麼做
const d = 7; // 一週有 7 天
// ✅ 說是什麼
const daysPerWeek = 7;// ❌ getList 沒說明什麼 list
function getList() {}
// ✅
function getPendingOrders() {}原則 2:避免資訊不足的名稱
// ❌ 要看上下文才知道
let data;
let info;
let temp;
let flag;
// ✅
let customerList;
let orderStatus;
let cachedResult;
let isShipmentReady;原則 3:用可搜尋的名稱
// ❌ 單一字母(除了迴圈 counter)
for (let i = 0; i < users.length; i++) {
const u = users[i];
process(u);
}
// ✅
for (const user of users) {
process(user);
}單字母變數在 grep 時找不到(會找到一大堆誤中)。
原則 4:動詞 vs 名詞
- 函式 = 動作 → 用動詞:
calculateTotal,sendEmail,isValid - 變數 / class = 東西 → 用名詞:
user,orderList,Database
// ❌ 函式用名詞
function orderTotal(order) {}
// ✅
function calculateOrderTotal(order) {}原則 5:布林用 is/has/can/should
// ❌ 不知道是什麼
let active;
let admin;
// ✅
let isActive;
let isAdmin;
let hasPermission;
let canEdit;
let shouldRetry;原則 6:別怕長名稱
// 長但清楚
const maxRetryAttemptsForPaymentProcessing = 3;
// 短但要查
const MAX_RETRY = 3;IDE 自動補全後打長名稱不費事,可讀性卻差很多。
原則 7:避免縮寫(除非是普遍共識)
- ✅
id,url,html,api— 大家都知道 - ❌
usr,addr,prm,calc— 為打字少 3 個字犧牲可讀性
命名的高階技巧:Domain Language
Ubiquitous Language(DDD 概念)
團隊用什麼詞跟業務溝通,code 裡就該用什麼詞。
業務跟你說:
「客戶下訂單,我們做出貨,之後客戶退貨。」
Code 不該變成:
// ❌ 自創詞彙
class UserRecord {}
class TransactionItem {}
class DeliveryOp {}
class ReturnProc {}Code 應該:
// ✅ 直接用業務詞
class Customer {}
class Order {}
class Shipment {}
class Return {}溝通成本直接降 90%。PM 讀 code 也讀得懂。
統一跨團隊的同一概念
同一個東西別有三個名字:
❌ 團隊裡:
- 後端叫
customer - 前端叫
user - 資料庫欄位叫
account
✅ 全部統一叫 customer(或 user,挑一個)。然後在程式碼各層強制執行。
區分相似概念
電商常見:
- Cart(購物車,結帳前可改)
- Order(訂單,結帳後確定)
- Shipment(出貨,物流狀態)
- Invoice(發票/收據,財務視角)
別寫 OrderStatus 包含「購物車」、「訂單」、「出貨」三層狀態。拆成不同概念。
抽象化常見反模式
反模式 1:過早抽象(Premature Abstraction)
看到一次用法就抽象成介面、工廠模式、抽象類。
結果:
- 抽象方向錯,未來真正需要擴充時不符合
- 讀者看一堆間接層找不到實作
- 修改要改多個檔案
等第三次使用再抽象。
反模式 2:錯誤抽象(Wrong Abstraction)
function sendMessage(type, recipient, content) {
if (type === 'email') { /* ... */ }
if (type === 'sms') { /* ... */ }
if (type === 'push') { /* ... */ }
}一段 if/else 不是抽象,是把 switch 藏起來。重構成策略模式或介面。
「重複不是抽象的敵人,錯誤的抽象才是。寧願重複 3 次,不要用錯的抽象綁死。」 — Sandi Metz
反模式 3:神名稱(Magic Names)
// 這個 Manager 到底 manage 什麼?
class DataManager {}
class UserController {}
class RequestHelper {}
class ThingProcessor {}Manager、Helper、Processor、Handler、Util 這類詞基本無資訊量。能取更具體的名字就取。
實戰練習方式
練習 1:讀自己半年前的 code
半年後你對當時的決策已經忘了。讀回去會立刻發現:
- 哪個名字看不懂
- 哪個抽象抽錯了
把這些修掉。做幾次,命名敏感度會爆增。
練習 2:讀大型開源專案
React、Vue、Django、Rails 這些專案的命名非常講究。讀源碼會吸收他們的命名品味。
練習 3:改名 refactor
挑一個 class / 函式 / 變數,只改名字,看會改到幾個檔案。
- 改到很多檔案 → 用量多,名字要更講究
- 改完發現有些呼叫很突兀 → 可能用錯抽象
練習 4:跟非技術人講你的功能
用非技術語言解釋你寫的功能 5 分鐘。解釋不清楚的地方,通常就是抽象或命名出問題。
實戰 Checklist
- 看到重複第三次才抽象,不急
- 抽象的名字能一句話講完它做什麼
- 命名用意圖而非實作細節
- 團隊用的業務詞直接用在 code(Ubiquitous Language)
- 避免 Manager / Helper / Processor 這種神名稱
- 布林加 is/has/can/should
- 半年後讀自己的 code,看哪裡看不懂就改名
相關文章
- Soft Skills Roadmap
- 需求拆解 — 歸納的第一步
- Clean Architecture — 抽象化的架構實踐
- Trade-off 框架
- 理解陌生 codebase 的方法