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 ได้ดีเท่าประโยคภาษาธรรมชาติ
ตัวอย่างและการ refactor
หัวข้อที่มีชื่อว่า “ตัวอย่างและการ refactor”ตัวอย่างที่ 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 จุดเดียว ไม่มีคอมเมนต์ให้ตกหล่นไม่อัปเดตตาม
ตัวอย่างที่ 2 — code ที่ถูกคอมเมนต์ปิดทิ้งไว้
หัวข้อที่มีชื่อว่า “ตัวอย่างที่ 2 — code ที่ถูกคอมเมนต์ปิดทิ้งไว้”ก่อน:
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 เสมือนสารบัญ