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

Dead Code

code ที่​ไม่​เคย​ถูก​เรียก​หรือ​ไม่มี​จุด​ประสงค์​อีก​ต่อ​ไป

Dead code คือ variable, parameter, field, method หรือ class ที่ ไม่​เคย​ถูกรัน ไป​ไม่​ถึง (unreachable) หรือ​ไม่มี​จุด​ประสงค์​อีก​ต่อ​ไป เพราะ​กลาย​เป็น​ของ​ล้าสมัย (refactoring.guru, SourceMaking) มัน​จัด​อยู่​ใน​กลุ่ม Dispensables — สิ่ง​ที่​ไม่​จำเป็น​และ​ควร​ลบ​ทิ้ง​เพื่อ​ให้ code เล็ก​ลง สะอาด​ขึ้น และ​เข้าใจ​ง่าย​ขึ้น

Wikipedia ให้​นิยาม​ที่​ครอบคลุม​กว่า​นั้น​เล็กน้อย: dead code คือ​คำ​สั่ง​ที่ ไม่​เคย​ถูกรัน​เลย หรือ​รัน​แล้ว​แต่​ผลลัพธ์​ไม่​เคย​ถูก​ใช้​ต่อ (Wikipedia) แบ่ง​ได้​เป็น​สอง​แบบ​ที่​ต่าง​กัน:

  • Unreachable code — คำ​สั่ง​ที่ control flow ไป​ไม่​ถึง​เลย เช่น branch หลัง return หรือ if (false) ตรวจ​จับ​ด้วย control-flow analysis
  • Dead store — ค่าที่​ถูก​คำนวณ​และ​เขียน​ลงตัวแปร แต่​ไม่มี​ใคร​อ่าน​ค่า​นั้น​ก่อน​จะ​ถูก​เขียน​ทับ​หรือ​ออก​จาก scope ตรวจ​จับ​ด้วย data-flow / live-variable analysis

สาเหตุ​ที่​พบ​บ่อย​ที่สุด​คือ requirement เปลี่ยน หรือ​มี​การ​แก้ไข bug แล้ว ไม่มี​ใคร​มี​เวลา​กลับ​มา​เก็บกวาด code เก่า feature ถูก​ปิด flag ถูก​ตรึง​ค่า​ไว้​ถาวร หรือ​มี​การ​เขียน code สำรอง “เผื่อ​ไว้” ที่​ไม่​เคย​ถูก​เรียก​จริง

  • Compiler/IDE warning ที่​ถูก​มอง​ข้าม​เป็น​ประจำ เช่น CS0162 (unreachable code detected), CS0169/CS0414 (field ถูก​กำหนด​ค่า​แต่​ไม่​เคย​ถูก​อ่าน), หรือ unused using — Visual Studio, Xcode และ Eclipse ตรวจ​จับ dead code ได้​ตั้งแต่​ตอน compile (Wikipedia)
  • “Find Usages” / “Find All References” ใน method class หรือ field แล้ว​เจอ 0 ผลลัพธ์​นอกจาก​ตัว​มัน​เอง
  • Code coverage report ที่​ชี้​ว่า​บาง method หรือ branch ไม่​เคย​ถูก hit เลย​ไม่​ว่า​จะ​รัน test suite หรือ traffic จริง​กี่​ครั้ง
  • Feature flag หรือ constant ที่​ตรึง​ค่า​ถาวร​แล้ว เช่น private const bool UseLegacyEngine = false; — code ใน branch ที่​คุม​ด้วย flag นี้​กลาย​เป็น dead code ทันที​ที่ flag ไม่มี​วัน​เปลี่ยน​อีก
  • Static analysis / linter เช่น Roslyn analyzers, ReSharper “unused member”, SonarQube dead-code rules ที่ scan ทั้ง solution แทน​การ​ไล่​ดู​ที​ละ file
  • คอมเมนต์​ประเภท // TODO: ลบหลัง migrate เสร็จ ที่​ค้าง​อยู่​หลาย release โดย​ไม่มี​ใคร​กลับ​มา​ทำ

Dead code ไม่​ได้​แค่​ทำให้ file ยาว​เกิน​จำเป็น — มัน​หลอก​ผู้​อ่าน ผู้​ที่มา​ใหม่​ไม่รู้​ว่า method นี้​ยัง​มี​ใคร​เรียก​อยู่​ไหม จึง​ต้อง​เสีย​เวลา​ไล่​อ่าน​และ​พิสูจน์​ที​ละ​ครั้ง​ว่า​มัน​ตาย​จริง​หรือ​แค่​ดูเหมือน​ตาย ทุก​ครั้ง​ที่​ทีม​แก้ business rule พวก​เขา​ต้อง​แก้ dead code คู่​ขนาน​ไป​ด้วย “เผื่อ​มัน​ยัง​ใช้​อยู่” ซึ่ง​เสีย​เวลา​โดย​เปล่า​ประโยชน์

อันตราย​ที่​ลึก​กว่า​นั้น​คือ ความ​ไม่​สอดคล้อง​ที่​แอบ​ซ่อน: เมื่อ business rule เปลี่ยน code ที่​ยัง live จะ​ถูก​อัปเดต​ตาม แต่ dead code (ที่​ดูเหมือน​ยัง live) มัก​ถูก​ละเลย ผล​คือ​มี logic สอง​ชุด​ที่​ขัดแย้ง​กัน​ใน file เดียวกัน — ถ้า​ใคร​เผลอ​เปิด​ใช้​มัน​ใหม่ (เช่น สลับ feature flag กลับ) จะ​ได้ behavior แบบ​เก่า​ที่​ผิด​เพี้ยน​ไป​จาก​ปัจจุบัน​แล้ว

Dead code ยัง​เพิ่ม​ขนาด build/binary โดย​ไม่​จำเป็น ทำให้ code coverage เป็น​ตัวเลข​ที่​มอง​แล้ว​เข้าใจ​ผิด (branch ที่​ไม่มี​วัน​ถูก execute ลาก​ค่า​เฉลี่ย coverage ลง) และ​เพิ่ม​ภาระ cognitive load ให้​ทุก​คน​ที่​ไล่​อ่าน code ฐาน​นี้​ใน​ระยะ​ยาว เหตุผล​ที่​คน​ไม่​กล้า​ลบ​มัก​มา​จาก​ความ​กลัว​ว่า “อาจ​ต้อง​ใช้​อีก​วัน​หนึ่ง” — ซึ่ง​ขัด​กับ​หลัก YAGNI โดยตรง เพราะ version control เก็บ​ประวัติ​ของ code ไว้​อยู่​แล้ว ลบ​แล้ว​ดึง​กลับ​มา​ได้​เสมอ​ถ้า​จำเป็น​จริง

code ด้าน​ล่าง​มี dead code สาม​ชนิด​ปน​กัน: legacy branch ที่​ถูก flag ปิด​ถาวร, method ที่​ไม่มี​ที่​เรียก​ใช้​เหลือ​อยู่​นอกจาก branch นั้น, และ field ที่​ถูก​กำหนด​ค่า​แต่​ไม่​เคย​ถูก​อ่าน

public class OrderProcessor
{
// flag นี้ถูกตรึงเป็น false ถาวรตั้งแต่ย้ายไป PricingEngine เมื่อปีก่อน
private const bool UseLegacyPricingEngine = false;
// ถูกกำหนดค่าใน constructor แต่ไม่มีจุดใดอ่านค่ามันอีก
private readonly ILogger _unusedLogger;
public OrderProcessor(ILogger logger)
{
_unusedLogger = logger;
}
public decimal CalculateTotal(Order order)
{
if (UseLegacyPricingEngine)
{
// ไปไม่ถึงตลอดกาล เพราะ UseLegacyPricingEngine เป็น false เสมอ
return CalculateLegacyDiscount(order);
}
return PricingEngine.Calculate(order);
}
// ไม่มีที่เรียกใช้เหลืออยู่นอกจาก branch ที่ unreachable ด้านบน
private decimal CalculateLegacyDiscount(Order order)
{
decimal discount = 0;
foreach (var item in order.Items)
{
if (item.Quantity > 10)
{
discount += item.UnitPrice * 0.05m;
}
}
return order.Subtotal - discount;
}
}

ใช้ Remove Dead Code (refactoring.com) ลบ​ทั้ง flag, branch ที่ unreachable, method ที่​ไม่มี​ผู้​เรียก และ field/constructor parameter ที่​ไม่​เคย​ถูก​อ่าน (เทียบเท่า Remove Parameter จาก refactoring.guru เมื่อ dependency ทั้ง​ตัว​ไม่มี​ใคร​ใช้​แล้ว):

public class OrderProcessor
{
public decimal CalculateTotal(Order order)
=> PricingEngine.Calculate(order);
}

ประวัติ​ของ CalculateLegacyDiscount และ UseLegacyPricingEngine ยัง​อยู่​ครบ​ใน git history หาก​วัน​หนึ่ง​ต้อง​ย้อน​ดู​ว่า legacy discount เคย​คำนวณ​อย่างไร ก็ git log -p หรือ git blame ดู​ได้​ทันที — ไม่​จำเป็น​ต้อง​แบก​มัน​ไว้​ใน file ที่​ใช้งาน​จริง

ใน​กรณี​ที่ dead code อยู่​ใน​รูป class หรือ interface ทั้ง​ตัว​ที่​ไม่มี​ใคร​ใช้ (เช่น subclass ของ hierarchy ที่​เหลือ implementation เดียว) refactoring.guru แนะนำ Inline Class หรือ Collapse Hierarchy แทน​การ​ลบ​ทิ้ง​ดื้อ ๆ เพื่อ​รวม behavior ที่​ยัง​จำเป็น​เข้า​กับ class ที่​เหลือ​อยู่​ก่อน​ค่อย​ลบ class เปล่า​ออก

flowchart LR
  A[Requirement เปลี่ยน] --> B[Flag หรือ Branch เก่าไม่ถูกใช้]
  B --> C[ไม่มีใครลบทิ้ง]
  C --> D[Dead Code สะสม]
  D --> E[ผู้อ่านสับสนว่ายังมีผลไหม]
  D --> F[Coverage และ Build ใหญ่ขึ้นโดยเปล่าประโยชน์]
  • YAGNI — เหตุผล​หลัก​ที่​ไม่​ควร​เก็บ code “เผื่อ​ใช้​ใน​อนาคต” ไว้​เลย
  • Boy Scout Rule — ทิ้ง code ให้​สะอาด​กว่า​ที่​เจอ รวม​ถึง​เก็บกวาด dead code ระหว่าง​ทาง
  • Refactoring — Remove Dead Code เป็น​หนึ่ง​ใน refactoring ที่​ปลอดภัย​ที่สุด​เมื่อ​มี test คุ้มกัน
  • Simplicity — code ที่​เรียบ​ง่าย​ที่สุด​คือ code ที่​ไม่มี​ส่วนเกิน
  • Comments — คอมเมนต์​ที่​อธิบาย code ตาย​ก็​ตาย​ไป​ด้วย เป็นกลิ่น​คู่​กัน​ที่​มัก​เจอ​พร้อม​กัน
  • Duplicate Code — code ซ้ำ​ที่​ถูก​ลืม​มัก​เป็น​แหล่ง​กำเนิด​ของ dead code เมื่อ code ต้นทาง​เปลี่ยน​แต่​สำเนา​ไม่​เปลี่ยน​ตาม