ก้าวเล็ก ๆ และ test
Refactoring ที่ทำได้ดีจะดูน่าเบื่อมาก คุณไม่ได้จ้อง function แล้วรัวพิมพ์ใหม่อย่างเร้าใจ แต่เปลี่ยนทีละนิด ยืนยันว่าไม่มีอะไรพัง แล้วบันทึกความคืบหน้าไว้ จากนั้นวนใหม่ ลูปนั้นคือ
- เปลี่ยน หนึ่ง อย่างที่เล็กและรักษา behavior ไว้
- รัน test
- ถ้าผ่าน ให้ commit ถ้าล้มเหลว ให้ undo แล้วก้าวให้เล็กลง
flowchart TB
A["Make ONE tiny<br/>behavior-preserving change"] --> B["Run the tests"]
B -->|green| C["Commit"]
B -->|red| D["Undo the last step"]
D --> E["Take a smaller step"]
E --> A
C --> F{"Done?"}
F -->|no| A
F -->|yes| G["Stop"] ทำไมต้องก้าวเล็ก ๆ?
หัวข้อที่มีชื่อว่า “ทำไมต้องก้าวเล็ก ๆ?”ขนาดของก้าวที่คุณก้าวกำหนดว่าคุณจะตกได้ไกลแค่ไหน ถ้าคุณเปลี่ยนห้าสิบบรรทัดแล้ว test กลายเป็นสีแดง บั๊กก็อาจอยู่ในบรรทัดไหนก็ได้ — คุณก็กลับไป debug ใหม่ ถ้าคุณเปลี่ยน หนึ่ง อย่างแล้ว test กลายเป็นสีแดง คุณรู้ทันทีว่าอะไรเป็นต้นเหตุ เพราะไม่มีอะไรอื่นขยับเลย undo ก้าวเดียวนั้น แล้วคุณก็กลับไปสู่โปรแกรมที่ทำงานได้ในทันที
ก้าวเล็ก ๆ รู้สึกช้ากว่า แต่จริง ๆ แล้วไม่ใช่ เวลาที่คุณ “เสีย” ไปกับการก้าวจิ๋ว ๆ หลายก้าวนั้นน้อยกว่าเวลาที่คุณเสียไปกับการตามล่า regression ในก้อน diff ที่ใหญ่และพันกันยุ่งเหยิงมาก ก้าวเล็ก ๆ แลกความรู้สึกว่าเร็วกับความเป็นจริงของการไม่เคยหลงทาง
ทำไมต้องมีตาข่ายความปลอดภัยจาก test?
หัวข้อที่มีชื่อว่า “ทำไมต้องมีตาข่ายความปลอดภัยจาก test?”หัวใจของนิยาม refactoring คือ การรักษา behavior และ test คือวิธี พิสูจน์ เรื่องนั้น หลังจบแต่ละก้าว แถบสีเขียวคือหลักฐานว่าการเปลี่ยนแปลงที่เพิ่งทำมองไม่เห็นจากภายนอก ถ้าไม่มี test คุณก็แค่ หวัง ว่า behavior ยังเหมือนเดิม และความหวังไม่ใช่ตาข่ายนิรภัย
ถ้า code ที่จะ refactor ยังไม่มี test เลย ให้เขียนขึ้นมาก่อน แค่ characterization test ไม่กี่ตัวก็พอ คือ test ที่ตรึงสิ่งที่ code ทำอยู่ วันนี้ ไว้ ไม่ว่าพฤติกรรมนั้นจะถูกหรือผิด (คำนี้มาจาก Michael Feathers ใน Working Effectively with Legacy Code, 2004) เท่านี้ก็ได้ตาข่ายที่ต้องใช้เพื่อปรับโครงสร้างอย่างมั่นใจแล้ว
ตัวอย่างก้าวเล็ก ๆ ที่ลงมือทำจริง
หัวข้อที่มีชื่อว่า “ตัวอย่างก้าวเล็ก ๆ ที่ลงมือทำจริง”เราจะเปลี่ยน conditional ที่อึดอัดซึ่งคืนค่าอัตราส่วนลด ให้เป็นโครงสร้างที่ชัดเจนและแบนราบ จับตาดูแต่ละก้าว: นำชื่อที่อธิบายความหมายเข้ามา แล้วทำให้การซ้อนกันแบนลง รัน test หลัง แต่ละ ก้าว (แสดงไว้ตรงนี้เป็นสถานะสุดท้าย แต่ที่จริงคุณจะ commit สองครั้ง)
// Beforefunction discount(total: number, member: boolean): number { let rate = 0; if (member) { if (total > 100) { rate = 0.2; } else { rate = 0.1; } } return total * rate;}
// Afterfunction discount(total: number, member: boolean): number { if (!member) return 0; const rate = total > 100 ? 0.2 : 0.1; return total * rate;}# Beforedef discount(total, member): rate = 0 if member: if total > 100: rate = 0.2 else: rate = 0.1 return total * rate
# Afterdef discount(total, member): if not member: return 0 rate = 0.2 if total > 100 else 0.1 return total * rate// Beforefunc Discount(total float64, member bool) float64 { rate := 0.0 if member { if total > 100 { rate = 0.2 } else { rate = 0.1 } } return total * rate}
// Afterfunc Discount(total float64, member bool) float64 { if !member { return 0 } rate := 0.1 if total > 100 { rate = 0.2 } return total * rate}// Beforefn discount(total: f64, member: bool) -> f64 { let mut rate = 0.0; if member { if total > 100.0 { rate = 0.2; } else { rate = 0.1; } } total * rate}
// Afterfn discount(total: f64, member: bool) -> f64 { if !member { return 0.0; } let rate = if total > 100.0 { 0.2 } else { 0.1 }; total * rate}input เดียวกันให้ส่วนลดเดียวกัน แต่ละสถานะระหว่างทางยังคอมไพล์ได้และผ่าน test — นั่นคือสิ่งที่ทำให้เราเดินหน้าต่อไปได้โดยไม่ต้องกลัว