Introduce Special Case
จุดประสงค์
หัวข้อที่มีชื่อว่า “จุดประสงค์”เอาค่าที่บางครั้งหายไป พร้อมกับ null check ที่ซ้ำอยู่ทุกจุดที่ใช้ค่านั้น มาแทนกรณีที่หายไปด้วย object จริงที่รู้คำตอบ default ที่ถูกต้อง object special case ตัวนี้ (มักเรียกว่า null object) ทำตาม interface เดียวกัน caller จึงเรียกใช้ได้ตามปกติ แล้ว branch แบบ “ถ้าไม่มีค่าจะทำยังไง” ก็หายไปจากทุกจุดที่เรียก ไปกระจุกอยู่ใน class เดียวแทน
Code Smell
หัวข้อที่มีชื่อว่า “Code Smell”นี่คือวิธีแก้อาการ check เคสพิเศษซ้ำ ๆ ซึ่งส่วนใหญ่ก็คือ if (x == null) พอ field หนึ่งหายไปได้ ทุก method ที่แตะ field นั้นก็งอก guard ตัวเดียวกันขึ้นมา แถมแต่ละตัวยังใส่ค่า default ของตัวเอง คือชื่อตรงนี้ plan ตรงนั้น ค่าบริการอีกที่หนึ่ง ความรู้ที่ว่า “ลูกค้าที่หายไปแปลว่าอะไร” จึงเลอะกระจายไปทั่ว codebase และทุกครั้งที่มี default ใหม่ก็ต้องไปเพิ่มอีกจุด object special-case รวบ default ทั้งหมดมาไว้ใน type เดียวที่ตอบกลับได้ตามปกติ
ก่อน → หลัง
หัวข้อที่มีชื่อว่า “ก่อน → หลัง”code แสดงผลอ่านชื่อและแพลนของลูกค้า โดยใช้ค่าดีฟอลต์สำหรับลูกค้าที่ไม่รู้จัก ก่อนหน้านี้ ทุกจุดตรวจสอบ null หลังจากนั้น UnknownCustomer ให้ค่าดีฟอลต์มา
// Beforefunction 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;}
// Afterclass 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;}# Beforedef customer_name(site): return "occupant" if site.customer is None else site.customer.name
def billing_plan(site): return "basic" if site.customer is None else site.customer.plan
# Afterclass UnknownCustomer: name = "occupant" plan = "basic" is_unknown = True
def customer_name(site): return site.customer.name
def billing_plan(site): return site.customer.plan// Beforefunc CustomerName(site Site) string { if site.Customer == nil { return "occupant" } return site.Customer.Name}
func BillingPlan(site Site) string { if site.Customer == nil { return "basic" } return site.Customer.Plan}
// Afterfunc UnknownCustomer() *Customer { return &Customer{Name: "occupant", Plan: "basic", IsUnknown: true}}
func CustomerName(site Site) string { return site.Customer.Name}
func BillingPlan(site Site) string { return site.Customer.Plan}// Beforefn customer_name(site: &Site) -> String { match &site.customer { None => "occupant".to_string(), Some(c) => c.name.clone(), }}
fn billing_plan(site: &Site) -> String { match &site.customer { None => "basic".to_string(), Some(c) => c.plan.clone(), }}
// Afterimpl Customer { fn unknown() -> Customer { Customer { name: "occupant".to_string(), plan: "basic".to_string(), is_unknown: true, } }}
fn customer_name(site: &Site) -> String { site.customer.name.clone()}
fn billing_plan(site: &Site) -> String { site.customer.plan.clone()}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 กลไกการทำงาน
หัวข้อที่มีชื่อว่า “กลไกการทำงาน”- เพิ่มพร็อพเพอร์ตีหรือ method ที่ให้ caller ถามได้ว่าค่าหนึ่งเป็นกรณีพิเศษหรือไม่ (ตัวอย่างเช่น
isUnknown) และทำให้ object ปกติตอบfalse - สร้าง object special-case — subclass, struct, หรือ factory — ที่ทำตาม interface เดียวกันและ return ค่าดีฟอลต์ที่ตกลงกันไว้สำหรับแต่ละ accessor
- จัดให้แหล่งที่มาของค่าส่ง object special-case กลับมาแทน null เมื่อค่านั้นหายไป
- แทนแต่ละจุด
if (x == null)ด้วยการใช้ค่านั้นโดยตรง ทีละจุด รัน test หลังแต่ละครั้ง - เมื่อการตรวจสอบที่ชัดแจ้งตัวสุดท้ายหายไป ให้ลบ code จัดการ null ที่ตายไปแล้วทิ้ง
ใช้เมื่อไหร่ / ข้อแลกเปลี่ยน
หัวข้อที่มีชื่อว่า “ใช้เมื่อไหร่ / ข้อแลกเปลี่ยน”หยิบ Introduce Special Case มาใช้เมื่อการตรวจสอบค่าที่หายไป ตัวเดียวกัน ซึ่งมีดีฟอลต์ ตัวเดียวกัน เกิดซ้ำข้ามหลายจุดที่เรียก การรวมศูนย์ค่าดีฟอลต์เหล่านั้นไว้ใน object เดียวขจัดความซ้ำซ้อน และให้ชื่อกับที่อยู่แก่กรณีที่หายไป
ท่านี้ไม่ได้เหมาะเสมอไป ถ้าค่าที่หายไปแปลคนละอย่างในแต่ละจุด คือเป็น default ตรงนี้แต่เป็น error ตรงนั้น object special-case ตัวเดียวก็รองรับไม่ไหว และการฝืนใช้ตัวเดียวจะกลบความต่างที่สำคัญ ส่วนค่าที่ถูก check แค่หนึ่งหรือสองจุด ใช้ guard clause ธรรมดาง่ายกว่า ให้ใช้ special case เมื่อ ความสม่ำเสมอ ของ default นั่นแหละคือเหตุผลที่คุ้มจะทำเป็น type
เนื้อหาที่เกี่ยวข้อง
หัวข้อที่มีชื่อว่า “เนื้อหาที่เกี่ยวข้อง”- Replace Nested Conditional with Guard Clauses
- Replace Conditional with Polymorphism
- Consolidate Conditional Expression
| ใช้ 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 จะซ้ำซ้อน