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,呼叫端全都不用動。

⚠️ 這招有一個明確的誤用邊界 → 特例物件只適合合法的空狀態,不適合查詢失敗