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 ถูกกำหนดค่าแต่ไม่เคยถูกอ่าน), หรือ unusedusing— 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 ไว้อยู่แล้ว ลบแล้วดึงกลับมาได้เสมอถ้าจำเป็นจริง
ตัวอย่างและการ refactor
หัวข้อที่มีชื่อว่า “ตัวอย่างและการ refactor”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 ต้นทางเปลี่ยนแต่สำเนาไม่เปลี่ยนตาม