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

ก้าวเล็ก ๆ และ test

Refactoring ที่ทำได้ดีจะดูน่าเบื่อมาก คุณไม่ได้จ้อง function แล้วรัวพิมพ์ใหม่อย่างเร้าใจ แต่เปลี่ยนทีละนิด ยืนยันว่าไม่มีอะไรพัง แล้วบันทึกความคืบหน้าไว้ จากนั้นวนใหม่ ลูปนั้นคือ

  1. เปลี่ยน หนึ่ง อย่างที่เล็กและรักษา behavior ไว้
  2. รัน test
  3. ถ้าผ่าน ให้ 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"]
จังหวะของ refactoring — เปลี่ยนเล็ก ๆ ทดสอบ commit ทำซ้ำจนเสร็จ

ขนาดของก้าวที่คุณก้าวกำหนดว่าคุณจะตกได้ไกลแค่ไหน ถ้าคุณเปลี่ยนห้าสิบบรรทัดแล้ว test กลายเป็นสีแดง บั๊กก็อาจอยู่ในบรรทัดไหนก็ได้ — คุณก็กลับไป debug ใหม่ ถ้าคุณเปลี่ยน หนึ่ง อย่างแล้ว test กลายเป็นสีแดง คุณรู้ทันทีว่าอะไรเป็นต้นเหตุ เพราะไม่มีอะไรอื่นขยับเลย undo ก้าวเดียวนั้น แล้วคุณก็กลับไปสู่โปรแกรมที่ทำงานได้ในทันที

ก้าวเล็ก ๆ รู้สึกช้ากว่า แต่จริง ๆ แล้วไม่ใช่ เวลาที่คุณ “เสีย” ไปกับการก้าวจิ๋ว ๆ หลายก้าวนั้นน้อยกว่าเวลาที่คุณเสียไปกับการตามล่า regression ในก้อน diff ที่ใหญ่และพันกันยุ่งเหยิงมาก ก้าวเล็ก ๆ แลกความรู้สึกว่าเร็วกับความเป็นจริงของการไม่เคยหลงทาง

หัวใจของนิยาม refactoring คือ การรักษา behavior และ test คือวิธี พิสูจน์ เรื่องนั้น หลังจบแต่ละก้าว แถบสีเขียวคือหลักฐานว่าการเปลี่ยนแปลงที่เพิ่งทำมองไม่เห็นจากภายนอก ถ้าไม่มี test คุณก็แค่ หวัง ว่า behavior ยังเหมือนเดิม และความหวังไม่ใช่ตาข่ายนิรภัย

ถ้า code ที่จะ refactor ยังไม่มี test เลย ให้เขียนขึ้นมาก่อน แค่ characterization test ไม่กี่ตัวก็พอ คือ test ที่ตรึงสิ่งที่ code ทำอยู่ วันนี้ ไว้ ไม่ว่าพฤติกรรมนั้นจะถูกหรือผิด เท่านี้ก็ได้ตาข่ายที่ต้องใช้เพื่อปรับโครงสร้างอย่างมั่นใจแล้ว

เราจะเปลี่ยน conditional ที่อึดอัดซึ่งคืนค่าอัตราส่วนลด ให้เป็นโครงสร้างที่ชัดเจนและแบนราบ จับตาดูแต่ละก้าว: นำชื่อที่อธิบายความหมายเข้ามา แล้วทำให้การซ้อนกันแบนลง รัน test หลัง แต่ละ ก้าว (แสดงไว้ตรงนี้เป็นสถานะสุดท้าย แต่ที่จริงคุณจะ commit สองครั้ง)

// Before
function discount(total: number, member: boolean): number {
let rate = 0;
if (member) {
if (total > 100) {
rate = 0.2;
} else {
rate = 0.1;
}
}
return total * rate;
}
// After
function discount(total: number, member: boolean): number {
if (!member) return 0;
const rate = total > 100 ? 0.2 : 0.1;
return total * rate;
}

input เดียวกันให้ส่วนลดเดียวกัน แต่ละสถานะระหว่างทางยังคอมไพล์ได้และผ่าน test — นั่นคือสิ่งที่ทำให้เราเดินหน้าต่อไปได้โดยไม่ต้องกลัว

ใช้ Small Steps + Tests เมื่อหลีกเลี่ยงเมื่อ
refactoring ครั้งใหญ่ที่แตะหลาย fileแก้ typo เล็กน้อยที่ test เห็นได้ชัด
code มี test coverage ดีพอที่จะ catch regressiontest suite ช้ามากจนทำ TDD ได้ยาก
ทีมมีหลายคน merge ใน branch เดียวกันงาน spike เพื่อ learn — ยังไม่ใช่ production

⚠️ ไม่ควร skip tests เมื่อ refactor เมื่อ:

  • code ที่แตะเป็น production path ที่สำคัญ
  • ไม่มี automated test และกำลัง refactor logic ที่ซับซ้อน
  • หลาย คนทำงานใน codebase เดียวกัน — regression ตาม track ยาก
สามจังหวะของจังหวะ refactoring คืออะไร?
ทำไมจึงควรเลือกก้าวเล็ก ๆ หลายก้าวมากกว่าการเปลี่ยนครั้งใหญ่ครั้งเดียว?
test มีบทบาทอะไรระหว่างการ refactoring?
ถ้า code ที่คุณต้องการ refactor ไม่มี test คุณควรทำอะไรก่อน?