ข้ามไปยังเนื้อหา

Introduce Special Case

เอาค่าที่บางครั้งหายไป พร้อมกับ null check ที่ซ้ำอยู่ทุกจุดที่ใช้ค่านั้น มาแทนกรณีที่หายไปด้วย object จริงที่รู้คำตอบ default ที่ถูกต้อง object special case ตัวนี้ (มักเรียกว่า null object) ทำตาม interface เดียวกัน caller จึงเรียกใช้ได้ตามปกติ แล้ว branch แบบ “ถ้าไม่มีค่าจะทำยังไง” ก็หายไปจากทุกจุดที่เรียก ไปกระจุกอยู่ใน class เดียวแทน

นี่คือวิธีแก้อาการ check เคสพิเศษซ้ำ ๆ ซึ่งส่วนใหญ่ก็คือ if (x == null) พอ field หนึ่งหายไปได้ ทุก method ที่แตะ field นั้นก็งอก guard ตัวเดียวกันขึ้นมา แถมแต่ละตัวยังใส่ค่า default ของตัวเอง คือชื่อตรงนี้ plan ตรงนั้น ค่าบริการอีกที่หนึ่ง ความรู้ที่ว่า “ลูกค้าที่หายไปแปลว่าอะไร” จึงเลอะกระจายไปทั่ว codebase และทุกครั้งที่มี default ใหม่ก็ต้องไปเพิ่มอีกจุด object special-case รวบ default ทั้งหมดมาไว้ใน type เดียวที่ตอบกลับได้ตามปกติ

code แสดงผลอ่านชื่อและแพลนของลูกค้า โดยใช้ค่าดีฟอลต์สำหรับลูกค้าที่ไม่รู้จัก ก่อนหน้านี้ ทุกจุดตรวจสอบ null หลังจากนั้น UnknownCustomer ให้ค่าดีฟอลต์มา

// Before
function customerName(site: Site): string {
return site.customer === null ? 'occupant' : site.customer.name;
}
function billingPlan(site: Site): string {
return site.customer === null ? 'basic' : site.customer.plan;
}
// After
class UnknownCustomer implements Customer {
readonly name = 'occupant';
readonly plan = 'basic';
readonly isUnknown = true;
}
function customerName(site: Site): string {
return site.customer.name;
}
function billingPlan(site: Site): string {
return site.customer.plan;
}
flowchart LR
  subgraph Before["Before"]
    A["customer == null<br/>? 'occupant'<br/>: customer.name"]
    B["customer == null<br/>? 'basic'<br/>: customer.plan"]
  end
  subgraph After["After"]
    C["customer.name"]
    D["customer.plan"]
    E["UnknownCustomer<br/>name = 'occupant'<br/>plan = 'basic'"]
  end
  Before -.->|"Introduce Special Case"| After
การตรวจสอบ null ที่กระจัดกระจายยุบรวมเป็น object special-case ตัวเดียวพร้อมค่าดีฟอลต์
  1. เพิ่มพร็อพเพอร์ตีหรือ method ที่ให้ caller ถามได้ว่าค่าหนึ่งเป็นกรณีพิเศษหรือไม่ (ตัวอย่างเช่น isUnknown) และทำให้ object ปกติตอบ false
  2. สร้าง object special-case — subclass, struct, หรือ factory — ที่ทำตาม interface เดียวกันและ return ค่าดีฟอลต์ที่ตกลงกันไว้สำหรับแต่ละ accessor
  3. จัดให้แหล่งที่มาของค่าส่ง object special-case กลับมาแทน null เมื่อค่านั้นหายไป
  4. แทนแต่ละจุด if (x == null) ด้วยการใช้ค่านั้นโดยตรง ทีละจุด รัน test หลังแต่ละครั้ง
  5. เมื่อการตรวจสอบที่ชัดแจ้งตัวสุดท้ายหายไป ให้ลบ code จัดการ null ที่ตายไปแล้วทิ้ง

หยิบ Introduce Special Case มาใช้เมื่อการตรวจสอบค่าที่หายไป ตัวเดียวกัน ซึ่งมีดีฟอลต์ ตัวเดียวกัน เกิดซ้ำข้ามหลายจุดที่เรียก การรวมศูนย์ค่าดีฟอลต์เหล่านั้นไว้ใน object เดียวขจัดความซ้ำซ้อน และให้ชื่อกับที่อยู่แก่กรณีที่หายไป

ท่านี้ไม่ได้เหมาะเสมอไป ถ้าค่าที่หายไปแปลคนละอย่างในแต่ละจุด คือเป็น default ตรงนี้แต่เป็น error ตรงนั้น object special-case ตัวเดียวก็รองรับไม่ไหว และการฝืนใช้ตัวเดียวจะกลบความต่างที่สำคัญ ส่วนค่าที่ถูก check แค่หนึ่งหรือสองจุด ใช้ guard clause ธรรมดาง่ายกว่า ให้ใช้ special case เมื่อ ความสม่ำเสมอ ของ default นั่นแหละคือเหตุผลที่คุ้มจะทำเป็น type

ใช้ Introduce Special Case เมื่อหลีกเลี่ยงเมื่อ
null check เดิมซ้ำในหลายจุดด้วย default เดียวกัน”หายไป” หมายความต่างกันในแต่ละจุดที่ใช้
ต้องการรวมศูนย์ default value ไว้ที่เดียวมีแค่ 1-2 จุดที่ check — guard clause ง่ายกว่า
object พิเศษให้ต้องการ behavior เฉพาะที่ implement ได้default ต่างกันในแต่ละ caller — object เดียวรองรับไม่ได้

⚠️ ไม่ควร Introduce Special Case เมื่อ:

  • การขาดหายไปของค่านั้นหมายถึง error จริง ๆ — ใช้ exception หรือ Result แทน
  • default value ในแต่ละ caller แตกต่างกัน — special case จะซ่อนความต่างที่สำคัญ
  • pattern ที่ใช้อยู่คือ Optional หรือ Result อยู่แล้ว — null object จะซ้ำซ้อน
Introduce Special Case แทนอะไร?
object special-case ควรทำตัวอย่างไรเมื่อ caller เรียกใช้?
เมื่อไรที่ object special-case ตัวเดียวเป็นทางเลือกที่ผิด?
อะไรมาแทนแต่ละจุด "if value is null" หลังการ refactor?