cover

結論先講

同一個功能,不同工程師寫出來的 code 可讀性可以差十倍。Junior 跟 Senior 的差異,很大部分不是演算法或框架,是兩件事:

  1. 抽象化:看出多個具體問題的共同結構
  2. 命名:給事物取一個一看就懂的名字

這兩件事被低估是因為「看起來沒做什麼事」。但你讀別人的 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');

這是壞的抽象。理由:

  1. 參數是字串 'email',TypeScript 檢查不到 typo
  2. 沒表達意圖getUserField(1, 'address') 讀不出來是要幹嘛
  3. 擴充困難:如果要組合多欄位?要格式化?要快取?

什麼是好的抽象

好的抽象看出共同結構,藏起不重要的細節,暴露語意

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 {}

ManagerHelperProcessorHandlerUtil 這類詞基本無資訊量。能取更具體的名字就取。


實戰練習方式

練習 1:讀自己半年前的 code

半年後你對當時的決策已經忘了。讀回去會立刻發現:

  • 哪個名字看不懂
  • 哪個抽象抽錯了

把這些修掉。做幾次,命名敏感度會爆增。

練習 2:讀大型開源專案

React、Vue、Django、Rails 這些專案的命名非常講究。讀源碼會吸收他們的命名品味。

練習 3:改名 refactor

挑一個 class / 函式 / 變數,只改名字,看會改到幾個檔案。

  • 改到很多檔案 → 用量多,名字要更講究
  • 改完發現有些呼叫很突兀 → 可能用錯抽象

練習 4:跟非技術人講你的功能

用非技術語言解釋你寫的功能 5 分鐘。解釋不清楚的地方,通常就是抽象或命名出問題。


實戰 Checklist

  • 看到重複第三次才抽象,不急
  • 抽象的名字能一句話講完它做什麼
  • 命名用意圖而非實作細節
  • 團隊用的業務詞直接用在 code(Ubiquitous Language)
  • 避免 Manager / Helper / Processor 這種神名稱
  • 布林加 is/has/can/should
  • 半年後讀自己的 code,看哪裡看不懂就改名

相關文章