Refactoring
ปรับปรุง design ของ code โดยไม่เปลี่ยนสิ่งที่มันทำ
คืออะไร
หัวข้อที่มีชื่อว่า “คืออะไร”Refactoring ใช้ได้ทั้งเป็นคำกริยาและคำนาม Martin Fowler ผู้เขียนหนังสือ Refactoring: Improving the Design of Existing Code ให้นิยามไว้ชัดเจนสองแบบ ในฐานะคำนาม มันคือ “การเปลี่ยนแปลงโครงสร้างภายในของซอฟต์แวร์ เพื่อให้เข้าใจง่ายขึ้นและแก้ไขได้ถูกลง โดยไม่เปลี่ยนพฤติกรรมที่สังเกตเห็นได้จากภายนอก” ในฐานะคำกริยา มันคือ “การปรับโครงสร้างซอฟต์แวร์ด้วยการใช้ refactoring ย่อย ๆ ต่อเนื่องกันเป็นชุด โดยไม่เปลี่ยนพฤติกรรมที่สังเกตเห็นได้”
หัวใจสำคัญคือคำว่า behavior-preserving — refactoring แต่ละครั้งเป็น transformation เล็ก ๆ ที่แทบไม่มีผลอะไรเดี่ยว ๆ แต่เมื่อทำต่อเนื่องกันเป็นลำดับ จะสามารถปรับโครงสร้าง code ขนาดใหญ่ได้อย่างปลอดภัย คำนี้ถูกใช้ในวรรณกรรมทางวิชาการครั้งแรกในบทความของ William Opdyke และ Ralph Johnson ปี 1990 ตามด้วยวิทยานิพนธ์ปริญญาเอกของ Opdyke ปี 1992 ที่วางรากฐานเชิงระบบสำหรับ object-oriented systems ก่อนที่หนังสือของ Fowler ในปี 1999 (และฉบับปรับปรุงปี 2018) จะทำให้แนวคิดนี้แพร่หลายในวงกว้างพร้อมกับ catalog ของ refactoring แต่ละแบบที่มีชื่อเรียกเฉพาะ เช่น Extract Function, Rename Variable, Replace Conditional with Polymorphism
Refactoring มักถูกเปรียบเทียบกับการ edit เอกสารที่เขียนขึ้นแล้ว เป้าหมายของการ edit ไม่ใช่การเปลี่ยนเจตนาโดยรวมของเนื้อหา แต่คือการทำให้มันดีขึ้น เช่นเดียวกัน การ refactor code ควรปรับปรุงความชัดเจน ความเรียบง่าย และความสามารถในการดูแลรักษา แต่ไม่ควรเปลี่ยนวัตถุประสงค์หรือพฤติกรรมของมันแม้แต่น้อย
Fowler ยังเน้นแนวคิด “สองหมวก” (two hats): เมื่อคุณกำลังเพิ่ม feature ใหม่ คุณสวมหมวกใบหนึ่ง — code ขยายความสามารถแต่โครงสร้างเดิมยังคงอยู่ เมื่อคุณกำลัง refactor คุณสวมอีกหมวกหนึ่ง — โครงสร้างเปลี่ยนไปแต่ความสามารถไม่เพิ่มขึ้นเลย นักพัฒนาที่ดีจะสลับหมวกไปมาบ่อย ๆ ใน session เดียว แต่ไม่ทำสองอย่างพร้อมกันในการแก้ไขครั้งเดียว เพราะจะทำให้แยกไม่ออกว่าถ้า test พัง เป็นเพราะ logic ใหม่ผิด หรือเพราะ refactor พลาด
ทำอย่างไร
หัวข้อที่มีชื่อว่า “ทำอย่างไร”เงื่อนไขที่ทำให้ refactor ปลอดภัยคือ ต้องมีตาข่ายนิรภัย โดยทั่วไปคือชุด automated test ที่ครอบคลุมพฤติกรรมที่สำคัญของ code อยู่แล้ว เมื่อ refactor เสร็จ เราจึงสามารถรัน test ซ้ำเพื่อยืนยันว่าพฤติกรรมยังคงเดิมไม่เปลี่ยนแปลง
refactor ใต้ test สีเขียวเท่านั้น: นี่คือเหตุผลที่ขั้น Refactor ใน Red · Green · Refactor มาหลังจาก test เป็นสีเขียวแล้วเสมอ ห้าม refactor ขณะ test กำลังแดง เพราะจะไม่รู้ว่าความล้มเหลวมาจาก code ใหม่ที่ยังไม่เสร็จ หรือจากการปรับโครงสร้างที่พลาด
แนวปฏิบัติที่ยึดถือกันทั่วไปมีดังนี้
- ทำทีละก้าวเล็กที่สุดเท่าที่ทำได้ — เช่น เปลี่ยนชื่อตัวแปรหนึ่งตัว แล้วรัน test ทันที แทนที่จะเปลี่ยนหลายอย่างพร้อมกันแล้วค่อยรัน test ทีเดียว
- รัน test หลังทุกก้าว ถ้า test แดง ให้ undo ทันทีแทนที่จะพยายามไล่ debug ต่อ — ก้าวที่แล้วมันเล็กพอที่จะ redo ใหม่ได้เร็วกว่าการตามหาว่าอะไรพัง
- commit บ่อย ๆ เพื่อให้มี checkpoint ย้อนกลับได้เสมอ
- ใช้ automated refactoring tool ของ IDE เช่น Extract Method, Rename, Extract Interface ใน Visual Studio หรือ Roslyn-based tooling ใน C# เพราะเครื่องมือเหล่านี้รับประกันความถูกต้องเชิง syntax/reference ได้ดีกว่าการแก้มือ
- แยก refactor ออกจาก feature commit อย่าปน code ที่เปลี่ยนพฤติกรรมกับ code ที่แค่จัดโครงสร้างใหม่ไว้ใน commit เดียวกัน เพื่อให้ code review และ git history อ่านง่าย
ตัวกระตุ้นทั่วไปที่บอกว่าถึงเวลา refactor คือการเจอ code smell เช่น Long Method หรือ Duplicate Code — สังเกตเห็นกลิ่นเมื่อไหร่ ควร refactor ทันทีตาม Boy Scout Rule แทนที่จะปล่อยผ่านแล้วรอวันไถ่บาปทีเดียวเป็นก้อนใหญ่
flowchart LR
A[มี test สีเขียวคุมอยู่] --> B[เลือก refactoring ก้าวเล็กที่สุด]
B --> C[แก้โครงสร้าง code]
C --> D[รัน test อีกครั้ง]
D -->|ผ่าน| E[commit]
D -->|ไม่ผ่าน| F[undo ทันที]
F --> B
E --> G{ยังมีกลิ่น code เหลืออยู่ไหม}
G -->|มี| B
G -->|ไม่มี| H[กลับไปทำงาน feature ต่อ]
ตัวอย่าง
หัวข้อที่มีชื่อว่า “ตัวอย่าง”ตัวอย่างคลาสสิกคือ Extract Method — ดึงกลุ่มคำสั่งที่ทำหน้าที่ชัดเจนออกมาเป็น method แยก แล้วตั้งชื่อให้สื่อเจตนา วิธีนี้เป็นตัวช่วยหลักในการลด Long Method และเปิดทางให้ reuse code ได้
ก่อน refactor — method เดียวทำหลายอย่างปนกัน (คำนวณ, กรอง, พิมพ์ผล):
public void PrintInvoice(Order order){ // คำนวณยอดรวมสินค้า decimal subtotal = 0; foreach (var item in order.Items) { subtotal += item.UnitPrice * item.Quantity; }
// คำนวณภาษี decimal tax = subtotal * 0.07m; decimal total = subtotal + tax;
// พิมพ์ผล Console.WriteLine($"Order #{order.Id}"); Console.WriteLine($"Subtotal: {subtotal:C}"); Console.WriteLine($"Tax: {tax:C}"); Console.WriteLine($"Total: {total:C}");}หลัง refactor ด้วย Extract Method หลายรอบ — พฤติกรรม (ผลลัพธ์ที่พิมพ์ออกมา) เหมือนเดิมทุกประการ แต่แต่ละ method สื่อเจตนาเดียวและ test แยกกันได้:
public void PrintInvoice(Order order){ decimal subtotal = CalculateSubtotal(order); decimal tax = CalculateTax(subtotal); decimal total = subtotal + tax;
PrintSummary(order.Id, subtotal, tax, total);}
private decimal CalculateSubtotal(Order order) => order.Items.Sum(item => item.UnitPrice * item.Quantity);
private decimal CalculateTax(decimal subtotal) => subtotal * TaxRate;
private const decimal TaxRate = 0.07m;
private void PrintSummary(int orderId, decimal subtotal, decimal tax, decimal total){ Console.WriteLine($"Order #{orderId}"); Console.WriteLine($"Subtotal: {subtotal:C}"); Console.WriteLine($"Tax: {tax:C}"); Console.WriteLine($"Total: {total:C}");}ในทางปฏิบัติ ไม่ควรพิมพ์ code ทั้ง block นี้ทีเดียวแล้วหวังว่า test จะผ่าน — ควรทำทีละ Extract Method หนึ่งครั้ง รัน test ยืนยันว่าผลลัพธ์ยังเหมือนเดิม แล้วค่อยทำขั้นถัดไป ซึ่ง Visual Studio และ IDE ยุคใหม่รองรับด้วยเมนู Quick Actions and Refactorings (Ctrl+.) ที่มีทั้ง Extract Method, Extract Interface, Inline Method และ Rename ให้ใช้แบบอัตโนมัติโดยไม่ต้องพิมพ์มือ
ประโยชน์และข้อควรระวัง
หัวข้อที่มีชื่อว่า “ประโยชน์และข้อควรระวัง”ประโยชน์
- ลด technical debt สะสม — Refactoring เป็นกระบวนการที่ควรทำ อย่างต่อเนื่อง ตามที่ design และบริบทซึ่งซอฟต์แวร์ทำงานอยู่ค่อย ๆ เคลื่อนเปลี่ยนไป การไม่ยอม refactor เพื่อปรับ design ให้เหมาะสมกับสภาพจะส่งผลให้เกิด technical debt ซึ่งบั่นทอน productivity และมักเป็นบ่อเกิดของ defect
- เพิ่มความสามารถในการอ่านและดูแลรักษา code ที่ผ่านการ refactor สม่ำเสมอจะสื่อเจตนาชัดเจนกว่า ลดเวลาที่ทีมต้องใช้ตีความก่อนแก้ไข
- เปิดทางให้ design pattern ทำงานได้เต็มที่ code ที่จัดโครงสร้างดีจะขยายและปรับใช้กับความต้องการใหม่ได้ง่ายกว่า
ข้อควรระวัง
- ห้าม refactor และเพิ่ม feature พร้อมกันในการแก้ไขครั้งเดียว เพราะจะแยกไม่ออกว่า test ที่พังมาจากส่วนไหน
- ต้องมี test คุมก่อนเสมอ ถ้าไม่มีตาข่ายนิรภัย การ “refactor” อาจกลายเป็นการเปลี่ยนพฤติกรรมโดยไม่ตั้งใจ ซึ่งไม่ใช่ refactoring อีกต่อไปแต่เป็นการแก้ code แบบเสี่ยง
- การ refactor ที่ execute แย่อาจทำร้าย architecture เช่น ทำให้ความสามารถในการดูแลรักษาแย่ลงถ้าไม่เข้าใจ dependency ของระบบดีพอ โดยเฉพาะเมื่อทีมมีการเปลี่ยนคนบ่อยจนความรู้เกี่ยวกับระบบขาดหาย
- ต้องแยกจาก big-bang rewrite refactoring คือก้าวเล็กต่อเนื่อง ไม่ใช่การหยุดพัฒนา feature แล้วเขียนระบบใหม่ทั้งหมด ซึ่งมีความเสี่ยงสูงกว่ามาก
ที่เกี่ยวข้อง
หัวข้อที่มีชื่อว่า “ที่เกี่ยวข้อง”- Red, Green, Refactor
- Test Driven Development
- Boy Scout Rule
- Don’t Repeat Yourself
- Long Method
- Duplicate Code