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

Comments

คอมเมนต์​ที่​ถูก​ใช้​แทน code ที่​ควร​ชัดเจน​ใน​ตัวเอง

Comments code smell ไม่​ได้ แปล​ว่า​คอมเมนต์​ทุก​อัน​แย่ — แต่​หมาย​ถึง​การ ใช้​คอมเมนต์​แทน code ที่​ชัดเจน เมื่อ​ต้อง​มี​คอมเมนต์​อธิบาย​ว่า code ทำ​อะไร (what) นั่น​มัก​เป็น​สัญญาณ​ว่า code สื่อ​ความ​ไม่​พอ ไม่ใช่​ว่า code ขาด​เอกสารประกอบ

Martin Fowler อธิบาย​ไว้​ใน​หนังสือ Refactoring ว่า​คอมเมนต์​มัก​ถูก​ใช้​เป็น “deodorant” — น้ำหอม​ที่​ฉีด​กลบ​กลิ่น code ที่​มี​ปัญหา แทนที่​จะ​แก้​ปัญหา​ที่​ต้นตอ เขา​บอกว่า “it’s surprising how often you look at thickly commented code and notice that the comments are there because the code is bad” กล่าว​คือ​ยิ่ง method มี​คอมเมนต์​อธิบาย​กลไก​ภายใน​เยอะ​เท่าไร ยิ่ง​มี​โอกาส​สูง​ที่​ตัว code เอง​มี​ปัญหา​เรื่อง​ความ​สื่อ​ความหมาย

refactoring.guru และ SourceMaking จัด​กลุ่ม Comments ไว้​ใน Dispensable smells — กลุ่ม​ของ​สิ่ง​ที่​ไม่​จำเป็น​และ​ควร​ถูก​กำจัด​ออก​เพื่อ​ให้ code สะอาด​และ​อ่าน​ง่าย​ขึ้น หลักการ​สำคัญ​ที่​ทั้ง​สอง​แหล่ง​ย้ำ​ตรง​กัน​คือ “the best comment is a good name for a method or class” — ชื่อ​ที่​ดี​ของ method หรือ class คือ​คอมเมนต์​ที่​ดี​ที่สุด เพราะ​มัน​อยู่​คู่​กับ code ตลอด​ไป​และ​ไม่มี​วัน​ตก​ยุค (out of sync) เหมือน​คอมเมนต์​ที่​เป็น​ข้อความ​แยก​ต่างหาก

code ที่​เขียน​ดี​มัก​ไม่​ต้อง​มี​คอมเมนต์​อธิบาย​กลไก เพราะ​ชื่อ method ตัวแปร และ type สื่อ​ความหมาย​เอง (ไม่​รวม doc comment ของ public API หรือ​คอมเมนต์​ที่​อธิบาย ทำไม — why) ดู Code Readability

สัญญาณ​ที่​บ่ง​บอกว่า​กำลัง​เผชิญ​กลิ่น​นี้:

  • method มี​คอมเมนต์​อธิบาย “ทำ​อะไร” อยู่​เหนือ​แต่ละ block code ราวกับ​เป็น​สารบัญ​ของ​สิ่ง​ที่​กำลัง​จะ​เกิด​ขึ้น
  • ต้อง​อ่าน​คอมเมนต์​ก่อน​ถึง​จะ​เข้าใจ​ว่า​นิพจน์ (expression) บรรทัด​ถัด​ไป​คำนวณ​อะไร
  • คอมเมนต์​บอกว่า code ส่วน​นี้ “ทำ X” แต่​ตัวแปร​หรือ​ชื่อ method ที่​ใช้​จริง​เป็น​ชื่อ​กำกวม​อย่าง data, temp, Process()
  • คอมเมนต์​ที่​ล้าสมัย (stale comment) — พูด​ถึง​พฤติกรรม​ที่ code ไม่​ได้​ทำ​แบบ​นั้น​แล้ว เพราะ​ไม่มี​ใคร​อัปเดต​คอมเมนต์​ตอน​แก้ code
  • คอมเมนต์​ปิด code เก่า​ทิ้ง​ไว้ (commented-out code) แทนที่​จะ​ลบ​ออก​จริง ๆ — ดู Dead Code
  • คอมเมนต์​ยาว​เป็น​พารา graph เพื่อ​อธิบาย method ที่​ทำ​หลาย​อย่าง​ใน​ตัว​เดียว มัก​คู่​กับ Long Method
  • คอมเมนต์​บอก​กฎ​ที่​ระบบ​ต้อง​เป็น​จริง​เสมอ (invariant) แต่​ไม่มี​การ​บังคับ​ใช้​กฎ​นั้น​ใน code จริง
  • ตก​ยุค​ง่าย — คอมเมนต์​ไม่​ได้​ถูก compiler ตรวจสอบ เมื่อ code เปลี่ยน​แต่​คอมเมนต์​ไม่​เปลี่ยน​ตาม จะ​กลาย​เป็น​ข้อมูล​ที่​ผิด​และ​หลอก​ผู้​อ่าน​รุ่น​ถัด​ไป
  • กลบ​อาการ​แทนที่​จะ​รักษา​โรค — การ​เขียน​คอมเมนต์​อธิบาย code ที่​งง​เป็นการ​ยอมรับ​ว่า code อ่าน​ยาก แต่​แก้​ที่ “อาการ” ไม่ใช่ “สาเหตุ” ซึ่ง​คือ​โครงสร้าง​และ​การ​ตั้ง​ชื่อ​ที่​แย่
  • เพิ่ม​ภาระ​การ​ดูแล — ทุก​ครั้ง​ที่​แก้ code ต้อง​จำ​ไว้​ว่า​ต้อง​แก้​คอมเมนต์​คู่​กัน​ด้วย เป็น​ภาระ​ซ้ำซ้อน​ที่​ชื่อ code ที่​ดี​ไม่​ต้อง​มี
  • บังตา​อาการ​อื่น ๆ — block ที่​ต้อง​มี​คอมเมนต์​อธิบาย​มัก​เป็น​สัญญาณ​ของ Long Method หรือ Poor Names ที่​ซ่อน​อยู่ ถ้า​รีบ​เขียน​คอมเมนต์​ปิด​เรื่อง​ไป ปัญหา​เชิง​โครงสร้าง​จริง ๆ จะ​ไม่​ถูก​แก้
  • ทำให้ review ยาก — ผู้​ทวน code ต้อง​เทียบ​คอมเมนต์​กับ code จริง​ว่า​ตรง​กัน​ไหม แทนที่​จะ​อ่าน code แล้ว​เข้าใจ​ได้​ทันที เพิ่ม cognitive load โดย​ไม่​จำเป็น

ข้อ​ยกเว้น​ที่​ยัง​จำเป็น: คอมเมนต์​ที่​อธิบาย ทำไม (เช่น เหตุผล​ทาง​ธุรกิจ, workaround ของ bug ใน library ภายนอก, การ​ตัดสิน​ใจ​เชิง​สถาปัตยกรรม) หรือ doc comment (XML doc, docstring) ของ public API ที่​ผู้​ใช้​ต้อง​อ่าน​โดย​ไม่​เปิด source code สิ่ง​เหล่า​นี้​ไม่ใช่​กลิ่น​เสีย เพราะ​ไม่มี​วิธี​สื่อ “ทำไม” ผ่าน​ชื่อ code ได้​ดี​เท่า​ประโยค​ภาษา​ธรรมชาติ

ตัวอย่าง​ที่ 1 — คอมเมนต์​อธิบาย “ทำ​อะไร” แทน code ที่​ควร​สื่อ​ความ​เอง

หัวข้อ​ที่​มีชื่อ​ว่า “ตัวอย่าง​ที่ 1 — คอมเมนต์​อธิบาย “ทำ​อะไร” แทน code ที่​ควร​สื่อ​ความ​เอง”

ก่อน — คอมเมนต์​ทำ​หน้าที่​เป็น​สารบัญ​ของ​แต่ละ block และ​มี magic number ที่​ต้อง​อาศัย​คอมเมนต์​ช่วย​ตีความ:

public class OrderProcessor
{
public decimal CalculateTotal(Order order)
{
// รวมยอดสินค้าทั้งหมดในออเดอร์
decimal total = 0;
foreach (var item in order.Items)
{
total += item.Price * item.Quantity;
}
// ตรวจสอบว่าลูกค้าเป็นสมาชิก VIP หรือไม่ ถ้าใช่ให้ลดราคา 15%
// ถ้าไม่ใช่ VIP แต่ยอดซื้อเกิน 1000 ให้ลด 5%
decimal discount = 0;
if (order.Customer.MembershipLevel == 3)
{
discount = total * 0.15m;
}
else if (total > 1000)
{
discount = total * 0.05m;
}
// ยอดสุทธิหลังหักส่วนลดต้องไม่ติดลบ
decimal finalTotal = total - discount;
return finalTotal;
}
}

หลัง — ใช้ Extract Method แยก​แต่ละ block ออก​เป็น method ที่​ตั้ง​ชื่อ​จาก​ข้อความ​ใน​คอมเมนต์​เดิม ใช้ Extract Variable ตั้ง​ชื่อ​เงื่อนไข ใช้ Rename Method/ตั้ง​ชื่อ constant แทน magic number และ​ปิด​ท้าย​ด้วย Introduce Assertion แทน​คอมเมนต์​ที่​บอก​กฎ invariant:

public class OrderProcessor
{
private const int VipMembershipLevel = 3;
private const decimal VipDiscountRate = 0.15m;
private const decimal BulkDiscountRate = 0.05m;
private const decimal BulkDiscountThreshold = 1000m;
public decimal CalculateTotal(Order order)
{
decimal subtotal = CalculateSubtotal(order);
decimal discount = CalculateDiscount(order.Customer, subtotal);
decimal finalTotal = subtotal - discount;
Debug.Assert(finalTotal >= 0, "ยอดสุทธิหลังหักส่วนลดต้องไม่ติดลบ");
return finalTotal;
}
private decimal CalculateSubtotal(Order order)
{
return order.Items.Sum(item => item.Price * item.Quantity);
}
private decimal CalculateDiscount(Customer customer, decimal subtotal)
{
bool isVipMember = customer.MembershipLevel == VipMembershipLevel;
bool qualifiesForBulkDiscount = subtotal > BulkDiscountThreshold;
if (isVipMember)
{
return subtotal * VipDiscountRate;
}
if (qualifiesForBulkDiscount)
{
return subtotal * BulkDiscountRate;
}
return 0m;
}
}

ผลลัพธ์: CalculateTotal อ่าน​แล้ว​เข้าใจ flow ทั้งหมด​ใน​สาม​บรรทัด​โดย​ไม่​ต้อง​เปิด​คอมเมนต์​เลย ส่วน​รายละเอียด​การ​คำนวณ​ย้าย​ไป​อยู่​ใน method ที่​ชื่อ​บอก​เจตนา​อยู่​แล้ว หาก​กฎ​ธุรกิจ​เปลี่ยน (เช่น อัตราส่วนลด VIP) ก็​แก้​ที่ constant จุด​เดียว ไม่มี​คอมเมนต์​ให้​ตกหล่น​ไม่​อัปเดต​ตาม

ก่อน:

public void SendInvoice(Order order)
{
// var oldFormatter = new LegacyInvoiceFormatter();
// oldFormatter.Format(order);
// EmailService.SendLegacy(oldFormatter.Output);
var formatter = new InvoiceFormatter();
EmailService.Send(formatter.Format(order));
}

หลัง — ลบ code ที่​ถูก​คอมเมนต์​ปิด​ออก​ทั้งหมด เพราะ source control (git) เก็บ​ประวัติ​ไว้​ให้​อยู่​แล้ว หาก​ต้อง​ย้อน​ดู code เก่า​ให้​ใช้ git log/git blame แทน​การ​แบก code ตาย​ไว้​ใน file (ดู Dead Code):

public void SendInvoice(Order order)
{
var formatter = new InvoiceFormatter();
EmailService.Send(formatter.Format(order));
}

เมื่อ​รู้สึก​อยาก​เขียน​คอมเมนต์ ให้​ถาม​ก่อน​ว่า​คอมเมนต์​นั้น​อธิบาย​อะไร แล้ว​เลือก​วิธี​จัดการ​ที่​เหมาะสม:

flowchart TD
    A[รู้สึกอยากเขียนคอมเมนต์] --> B{คอมเมนต์อธิบายอะไร}
    B -->|นิพจน์ซับซ้อน| C[ใช้ Extract Variable]
    B -->|block code ทำอะไร| D[ใช้ Extract Method]
    D --> E[Rename Method ให้สื่อความ]
    B -->|กฎที่ต้องเป็นจริงเสมอ| F[ใช้ Introduce Assertion]
    B -->|เหตุผลว่าทำไมต้องทำแบบนี้| G[เก็บคอมเมนต์ไว้ได้]

หาก​คอมเมนต์​ตก​อยู่​ใน​กิ่ง C, D หรือ F แปล​ว่า​มัน​คือ​กลิ่น​เสีย​ที่​ควร refactor ทิ้ง มี​เพียง​กิ่ง G (อธิบาย “ทำไม”) เท่านั้น​ที่​คอมเมนต์​ยัง​คง​มี​คุณค่า

  • Code Readability — เป้าหมาย​ปลายทาง​ที่​การ​กำจัด​กลิ่น​นี้​มุ่ง​ไป​หา
  • Naming Things — ชื่อ​ที่​ดี​คือ​คอมเมนต์​ที่​ดี​ที่สุด
  • Obscured Intent — กลิ่น​พี่น้อง​ที่​ทำให้​ต้อง​พึ่ง​คอมเมนต์​อธิบาย​เจตนา
  • Poor Names — สาเหตุ​ทั่วไป​ที่​ทำให้​ต้อง​เขียน​คอมเมนต์​ชดเชย
  • Dead Code — กรณี​เฉพาะ​ของ code ที่​ถูก​คอมเมนต์​ปิด​ทิ้ง​ไว้​แทนที่​จะ​ลบ
  • Long Method — method ยาว​มัก​มา​พร้อม​คอมเมนต์​กำกับ​แต่ละ block เสมือน​สารบัญ