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

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 ใหม่​ที่​ยัง​ไม่​เสร็จ หรือ​จาก​การ​ปรับ​โครงสร้าง​ที่​พลาด

แนว​ปฏิบัติ​ที่​ยึดถือ​กัน​ทั่วไป​มี​ดังนี้

  1. ทำที​ละ​ก้าว​เล็ก​ที่สุด​เท่า​ที่​ทำได้ — เช่น เปลี่ยน​ชื่อ​ตัวแปร​หนึ่ง​ตัว แล้ว​รัน test ทันที แทนที่​จะ​เปลี่ยน​หลาย​อย่าง​พร้อม​กัน​แล้ว​ค่อย​รัน test ที​เดียว
  2. รัน test หลัง​ทุก​ก้าว ถ้า test แดง ให้ undo ทันที​แทนที่​จะ​พยายาม​ไล่ debug ต่อ — ก้าว​ที่​แล้ว​มัน​เล็ก​พอที่​จะ redo ใหม่​ได้​เร็ว​กว่า​การ​ตามหา​ว่า​อะไร​พัง
  3. commit บ่อย ๆ เพื่อ​ให้​มี checkpoint ย้อน​กลับ​ได้​เสมอ
  4. ใช้ automated refactoring tool ของ IDE เช่น Extract Method, Rename, Extract Interface ใน Visual Studio หรือ Roslyn-based tooling ใน C# เพราะ​เครื่องมือ​เหล่า​นี้​รับประกัน​ความ​ถูกต้อง​เชิง syntax/reference ได้​ดี​กว่า​การ​แก้มือ
  5. แยก 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 แล้ว​เขียน​ระบบ​ใหม่​ทั้งหมด ซึ่ง​มี​ความ​เสี่ยง​สูง​กว่า​มาก