Introduce Special Case:把特殊值做成同形狀的物件
適用訊號:同一個「特殊值」在很多地方被重複檢查,而每個檢查之後做的事都一樣。
// before:散落各處的同一種檢查
const aCustomer = site.customer
let customerName
if (aCustomer === 'unknown') customerName = 'occupant'
else customerName = aCustomer.name
let plan
if (aCustomer === 'unknown') plan = registry.billingPlans.basic
else plan = aCustomer.billingPlan做法是替特例做一個物件,讓它長得跟正常物件一樣,把「unknown 的時候該回什麼」寫進去一次:
class UnknownCustomer {
get isUnknown() { return true }
get name() { return 'occupant' }
get billingPlan() { return registry.billingPlans.basic }
set billingPlan(arg) { /* 忽略 */ }
get paymentHistory() { return new NullPaymentHistory() }
}
class NullPaymentHistory {
get weeksDelinquentInLastYear() { return 0 }
}由來源端負責把特殊值換成特例物件,呼叫端一個 if 都不用寫:
class Site {
get customer() {
return (this._customer === 'unknown') ? new UnknownCustomer() : this._customer
}
}
const aCustomer = site.customer
const customerName = aCustomer.name // 不用再問是不是 unknown不開 class 的版本
資料是純 record 時,回一個形狀一樣的物件字面值就夠了:
function createUnknownCustomer() {
return {
isUnknown: true,
name: 'occupant',
billingPlan: registry.billingPlans.basic,
paymentHistory: { weeksDelinquentInLastYear: 0 },
}
}差別只在求值時機:class 版是 lazy(getter 被呼叫才算),字面值版是當場把所有欄位算好。
要判斷「是不是特例」時留 isUnknown
// before
if (aCustomer === 'unknown') { /* ... */ }
// after
if (aCustomer.isUnknown) { /* ... */ }這樣特殊值長什麼樣只有 factory 知道;哪天 'unknown' 要換成 null 或某個 id,呼叫端全都不用動。
⚠️ 這招有一個明確的誤用邊界 → 特例物件只適合合法的空狀態,不適合查詢失敗