Replace Parameter with Query
จุดประสงค์
หัวข้อที่มีชื่อว่า “จุดประสงค์”ถ้า function คำนวณค่าของ parameter ตัวหนึ่งได้เองจากข้อมูลที่รับมาอยู่แล้ว parameter ตัวนั้นก็เป็นน้ำหนักส่วนเกิน caller ทุกตัวต้องคำนวณแล้วส่งเข้ามา และแต่ละตัวก็คำนวณไม่ตรงกันได้ ลบทิ้งไปเลย แล้วให้ function ถามหาค่าเอง — นั่นคือ query — จะได้เหลือ single source of truth และลดจุดที่ call site ทำพลาดไปอีกหนึ่งจุด
อาการของปัญหา
หัวข้อที่มีชื่อว่า “อาการของปัญหา”parameter ตัวหนึ่งถูกส่งมาแบบเดิมทุกครั้ง โดยคำนวณจาก argument อีกตัวที่ function มีอยู่แล้ว คุณจะเห็น caller รันการคำนวณสั้น ๆ ชุดเดียวกันก่อนเรียกทุกครั้ง แล้วยื่นผลลัพธ์เข้ามา ที่แย่กว่านั้นคือ caller สองตัวคำนวณไม่ตรงกันนิดหน่อย function จึงให้ผลไม่คงเส้นคงวาขึ้นกับว่าใครเป็นคนเรียก สรุปคือ parameter ตัวนี้เสนอความยืดหยุ่นที่ไม่มีใครต้องการจริง ๆ
ก่อน → หลัง
หัวข้อที่มีชื่อว่า “ก่อน → หลัง”finalPrice รับทั้ง quantity และ discount แต่ค่า discount ถูกกำหนดโดย quantity ทั้งหมด แล้ว caller ก็ต้องคำนวณเองทุกครั้ง เราจึงลบ parameter ทิ้งแล้วย้ายการคำนวณ discount เข้าไปข้างใน
// Beforefunction finalPrice(quantity: number, discount: number): number { const base = quantity * 5; return base - base * discount;}
const d = quantity > 100 ? 0.1 : 0;const price = finalPrice(quantity, d);
// Afterfunction finalPrice(quantity: number): number { const base = quantity * 5; return base - base * discountFor(quantity);}
function discountFor(quantity: number): number { return quantity > 100 ? 0.1 : 0;}
const price = finalPrice(quantity);# Beforedef final_price(quantity, discount): base = quantity * 5 return base - base * discount
d = 0.1 if quantity > 100 else 0price = final_price(quantity, d)
# Afterdef final_price(quantity): base = quantity * 5 return base - base * discount_for(quantity)
def discount_for(quantity): return 0.1 if quantity > 100 else 0
price = final_price(quantity)// Beforefunc FinalPrice(quantity int, discount float64) float64 { base := float64(quantity) * 5 return base - base*discount}
d := 0.0if quantity > 100 { d = 0.1}price := FinalPrice(quantity, d)
// Afterfunc FinalPrice(quantity int) float64 { base := float64(quantity) * 5 return base - base*discountFor(quantity)}
func discountFor(quantity int) float64 { if quantity > 100 { return 0.1 } return 0}
price := FinalPrice(quantity)// Beforefn final_price(quantity: i32, discount: f64) -> f64 { let base = quantity as f64 * 5.0; base - base * discount}
let d = if quantity > 100 { 0.1 } else { 0.0 };let price = final_price(quantity, d);
// Afterfn final_price(quantity: i32) -> f64 { let base = quantity as f64 * 5.0; base - base * discount_for(quantity)}
fn discount_for(quantity: i32) -> f64 { if quantity > 100 { 0.1 } else { 0.0 }}
let price = final_price(quantity);flowchart LR
subgraph Before["Before"]
A["finalPrice(qty, discount)"] --> B["caller computes discount<br/>from qty first"]
end
subgraph After["After"]
C["finalPrice(qty)"] --> D["derives discount<br/>internally"]
end
Before -.->|"Replace Parameter with Query"| After กลไกการทำงาน
หัวข้อที่มีชื่อว่า “กลไกการทำงาน”- ยืนยันว่า parameter คำนวณได้ครบจากข้อมูลที่ function มีอยู่แล้ว ไม่ว่าจะเป็น parameter ตัวอื่น field หรือ query ที่เข้าถึงได้ ถ้าค่ายังขึ้นกับอะไรภายนอก ให้หยุดก่อน เพราะลบแล้วจะลักลอบพา dependency ที่ซ่อนอยู่เข้ามา
- แยกการคำนวณออกมาเป็น query ถ้ายังไม่ได้ทำ
- ข้างใน function แทนที่การใช้ parameter แต่ละจุดด้วยการเรียก query นั้น
- ลบ parameter ออกจาก signature แล้วลบการคำนวณที่ตอนนี้ตายแล้วออกจาก caller ทุกราย รัน test หลังแต่ละ caller
ใช้เมื่อไหร่ / ข้อแลกเปลี่ยน
หัวข้อที่มีชื่อว่า “ใช้เมื่อไหร่ / ข้อแลกเปลี่ยน”ใช้สิ่งนี้เมื่อ caller คำนวณ argument ในแบบเดียวกันทั้งหมด, เมื่อค่าถูกกำหนดทั้งหมดโดยข้อมูลที่ function เข้าถึงได้ หรือเมื่อคุณต้องการลบความเสี่ยงที่ caller จะส่งค่าที่ไม่สอดคล้องกัน parameter ที่น้อยลงหมายถึง interface ที่เรียบง่ายและใช้ผิดได้ยากขึ้น
ท่ากลับกันคือ Replace Query with Parameter บางครั้งการคำนวณค่า ข้างใน function ก็สร้าง dependency ที่ซ่อนอยู่ เช่น function เอื้อมไปหา global state นาฬิกา หรือ singleton ซึ่งทำให้ทดสอบยากและตามเหตุผลยาก กรณีนั้นให้ทำกลับด้าน คือยกการคำนวณออกมา รับค่าเป็น parameter แล้วให้ caller เป็นคนจัดหาให้ เท่ากับแลก signature ที่เพรียวกับ dependency ที่ชัดเจนและ inject ได้ เลือกเอาว่าตรงนั้นคุณให้ค่ากับอะไรมากกว่า ระหว่าง interface ที่เล็ก กับ function ที่ไม่มีการเอื้อมแอบ ๆ
เนื้อหาที่เกี่ยวข้อง
หัวข้อที่มีชื่อว่า “เนื้อหาที่เกี่ยวข้อง”| ใช้ Replace Parameter with Query เมื่อ | หลีกเลี่ยงเมื่อ |
|---|---|
| ทุก caller คำนวณ argument แบบเดียวกัน | การคำนวณนั้นเข้าถึง global state หรือ clock |
| ค่านั้นกำหนดได้ทั้งหมดจากข้อมูลที่ function มีอยู่ | caller แต่ละรายมี logic ต่างกัน |
| ต้องการลดโอกาสที่ caller จะส่งค่าไม่สอดคล้อง | ต้องการ inject dependency ผ่าน parameter เพื่อ test |
⚠️ ไม่ควร Replace Parameter with Query เมื่อ:
- การคำนวณข้างใน function ซ่อน dependency (global, clock, DB) — ทำให้ test ยาก
- บางครั้งต้องการ override ค่าที่คำนวณได้ — เช่น test ที่ inject ค่าเฉพาะ
- parameter นั้นคือ seam ที่ทำให้เขียน unit test ได้ — อย่าลบ