ก้าวเล็ก ๆ และ 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 ทำอยู่ วันนี้ ไว้ ไม่ว่าพฤติกรรมนั้นจะถูกหรือผิด เท่านี้ก็ได้ตาข่ายที่ต้องใช้เพื่อปรับโครงสร้างอย่างมั่นใจแล้ว
ตัวอย่างก้าวเล็ก ๆ ที่ลงมือทำจริง
หัวข้อที่มีชื่อว่า “ตัวอย่างก้าวเล็ก ๆ ที่ลงมือทำจริง”เราจะเปลี่ยน 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 — นั่นคือสิ่งที่ทำให้เราเดินหน้าต่อไปได้โดยไม่ต้องกลัว
เนื้อหาที่เกี่ยวข้อง
หัวข้อที่มีชื่อว่า “เนื้อหาที่เกี่ยวข้อง”| ใช้ Small Steps + Tests เมื่อ | หลีกเลี่ยงเมื่อ |
|---|---|
| refactoring ครั้งใหญ่ที่แตะหลาย file | แก้ typo เล็กน้อยที่ test เห็นได้ชัด |
| code มี test coverage ดีพอที่จะ catch regression | test suite ช้ามากจนทำ TDD ได้ยาก |
| ทีมมีหลายคน merge ใน branch เดียวกัน | งาน spike เพื่อ learn — ยังไม่ใช่ production |
⚠️ ไม่ควร skip tests เมื่อ refactor เมื่อ:
- code ที่แตะเป็น production path ที่สำคัญ
- ไม่มี automated test และกำลัง refactor logic ที่ซับซ้อน
- หลาย คนทำงานใน codebase เดียวกัน — regression ตาม track ยาก