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

Code Readability

เขียน​เพื่อ​ให้​คน​อ่าน​เข้าใจ ไม่ใช่​แค่​ให้​เครื่อง​รัน​ได้

Code Readability คือ​คุณสมบัติ​ของ source code ที่​ทำให้​นัก​พัฒนา​คน​อื่น (หรือ​แม้แต่​ตัว​เรา​เอง​ใน​อีก​หก​เดือน​ข้าง​หน้า) สามารถ​อ่าน​แล้ว​เข้าใจ​เจตนา​และ​พฤติกรรม​ของ code ได้​อย่าง​รวดเร็ว โดย​แทบ​ไม่​ต้อง​พึ่ง​เอกสาร​ภายนอก​หรือ​คอมเมนต์​อธิบาย​ทุก​บรรทัด นี่​ไม่ใช่​แค่​เรื่อง “สวยงาม” แต่​เป็น​ปัจจัย​ที่​ส่ง​ผล​โดยตรง​ต่อ​ต้นทุน​การ​ดูแล​รักษา​ซอฟต์แวร์​ใน​ระยะ​ยาว เพราะ code ถูก อ่าน บ่อย​กว่า​ถูก เขียน หลาย​เท่าตัว​ตลอด​วงจร​ชีวิต​ของ​มัน

Martin Fowler สรุป​แนวคิด​นี้​ไว้​อย่าง​คมคาย​ว่า “Any fool can write code that a computer can understand. Good programmers write code that humans can understand.” — ใคร​ก็ตาม​ที่​โง่​พอ​ก็​เขียน code ที่​เครื่อง​เข้าใจ​ได้ แต่​โปรแกรมเมอร์​ที่​ดี​เขียน code ที่​มนุษย์​เข้าใจ​ได้ ส่วน Abelson และ Sussman ใน​หนังสือ Structure and Interpretation of Computer Programs ก็​ย้ำ​แนวคิด​เดียวกัน​ว่า โปรแกรม​ควร​ถูก​เขียน​ขึ้น​เพื่อ​ให้​คน​อ่าน​เป็น​หลัก และ​เป็น​เพียง​ผลพลอยได้​ที่​เครื่อง​จะ​เอา​ไป​รัน

แนวคิด​ที่​อยู่​เบื้องหลัง​เรื่อง​นี้​คือ cognitive load — ภาระ​ทาง​สมอง​ที่​ผู้​อ่าน​ต้อง​แบกรับ​เพื่อ​ทำความ​เข้าใจ code ชิ้น​หนึ่ง ยิ่ง code ต้อง​ให้​ผู้​อ่าน​จำ context เยอะ ไล่​ตาม​การ​เรียก function ข้าม file หลาย​ชั้น หรือ​ถอดรหัส​ชื่อ​ตัวแปร​ที่​คลุมเครือ​มาก​เท่าไร cognitive load ก็​ยิ่ง​สูง​ขึ้น​เท่านั้น ส่ง​ผล​ให้ code review ช้า​ลง bug แฝง​ตัว​ได้​ง่าย​ขึ้น และ​การ refactor มี​ความ​เสี่ยง​มาก​ขึ้น

แม้​จะ​มี​เหตุผล​ที่​สม​เหตุ​สม​ผล​ด้าน​ประสิทธิภาพ​หรือ​การ​ป้องกัน​การ​ขโมย​ทรัพย์สิน​ทาง​ปัญญา ที่​ทำให้​ต้อง minify หรือ​ถึง​ขั้น obfuscate code ตอน deploy แต่ source ที่​ทีม​ทำงาน​ด้วย​จริง ๆ ควร​ถูก​รักษา​ให้​อยู่​ใน​สภาพ​อ่าน​ง่าย​และ​เป็น​ระเบียบ​ที่สุด​เท่า​ที่​จะ​ทำได้​เสมอ

การ​ยก​ระดับ​ความ​อ่าน​ง่าย​ของ code ทำได้​หลาย​ทาง โดย​มัก​จะ​ประกอบ​กัน​เป็น​ชุด​ของ​ธรรมเนียม​ปฏิบัติ ไม่ใช่​กฎ​เดี่ยว ๆ:

1. ตั้ง​ชื่อ​ให้​สื่อ​เจตนา (Naming) — ชื่อ​ตัวแปร function class ควร​บอก​ได้​ทันที​ว่า​มัน​คือ​อะไร​หรือ​ทำ​อะไร หลีก​เลี่ยง​ชื่อ​สั้น​และ​ลึกลับ​อย่าง d, tmp, data2 เว้น​แต่ในสโคป​ที่​แคบ​มาก​และ​อายุ​สั้น (เช่น​ตัว​นับ loop) ยิ่งสโคป​ของ​ตัวแปร​กว้าง​เท่าไร ชื่อ​ก็​ควร​สื่อ​ความหมาย​ชัดเจน​มาก​ขึ้น​เท่านั้น

2. ให้ function/method ทำ​สิ่ง​เดียว — ใน​อุดมคติ function ควร​ทำ​สิ่ง​เดียว ชื่อ​ของ​มัน​ควร​สะท้อน​สิ่ง​ที่​มัน​ทำ และ​มัน​ควร​อยู่​ใน class ที่​เล็ก​และ​มี cohesion สูง​ต่อ​พฤติกรรม​ที่ function ห่อ​หุ้ม​ไว้ function สั้น ๆ ที่​อ่าน​จบ​ได้​โดย​ไม่​ต้อง​เลื่อน​จอ มัก​เข้าใจ​ง่าย​กว่า function ยาว​ที่​คลุม​หลาย​ความ​รับผิดชอบ

3. รักษา abstraction level ให้​สม่ำเสมอ​ใน​แต่ละ function — อย่า​ผสม code ระดับ​สูง (เช่น เรียก ProcessOrder()) กับ code ระดับ​ต่ำ (เช่น การ parse string ทีละ character) ไว้​ใน function เดียวกัน เพราะ​จะ​ทำให้​ผู้​อ่าน​ต้อง​สลับ “ระดับ​ความคิด” ไปมา

4. ลด conditional complexity — เงื่อนไข​ซ้อน​กัน​หลาย​ชั้น (nested if/else) หรือ boolean logic ที่​ซับซ้อน​เป็น​ศัตรู​ตัว​ฉกาจ​ของ​ความ​อ่าน​ง่าย ควร​ใช้ guard clause, สกัด​เงื่อนไข​ออก​เป็น method ที่​ตั้ง​ชื่อ​สื่อ​ความหมาย หรือ​ใช้ polymorphism แทน switch/if ยาว ๆ เมื่อ​เหมาะสม

5. ใช้​ธรรมเนียม​การ​เขียน code (coding conventions) ที่​สอดคล้อง​กัน​ทั้ง​ทีม — ไม่​เพียง​แค่​การ​ตั้ง​ชื่อ​ตัวแปร แต่​รวม​ถึง​การ​ใช้​ตัวพิมพ์ (PascalCase, camelCase ฯลฯ) การ​ย่อหน้า การ​จัด​วาง whitespace และ​รูปแบบ​การ​ขึ้น​บรรทัด สิ่ง​เหล่า​นี้​ล้วน​ส่ง​ผล​ต่อ​ความ​ง่าย​ใน​การ​เข้าใจ​ว่า​โปรแกรม​กำลัง​ทำ​อะไร — ตรง​กับ​ที่ Wikipedia นิยาม coding conventions ว่า​เป็น​ชุด​แนวทาง​ที่​ครอบคลุม​ตั้งแต่ file organization, indentation, comments ไป​จนถึง naming conventions โดย​มี​เป้าหมาย​เพื่อ improve readability และ​ทำให้ software maintenance ง่าย​ขึ้น

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

7. ใช้​เครื่องมือ​ช่วย​ตรวจสอบ​และ​จัด format อัตโนมัติ — linter, formatter (เช่น dotnet format, EditorConfig) และ static analyzer ช่วย​บังคับ​ใช้​ธรรมเนียม​ร่วม​กัน​โดย​ไม่​ต้อง​พึ่ง​วินัย​ส่วน​บุคคล​ล้วน ๆ ลด​การ​ถกเถียง​เรื่อง style ใน code review และ​ปล่อย​ให้​ทีม​โฟกัส​กับ logic แทน

ต่อ​ไป​นี้​เป็น​ภาพ​รวม​ของ​กระบวนการ​ตัดสิน​ใจ​เมื่อ​พิจารณา​ว่า​จะ​ปรับปรุง​ความ​อ่าน​ง่าย​ของ code ชิ้น​หนึ่ง​ตรง​ไหน​ก่อน:

flowchart TD
    Start[อ่าน code ที่มีอยู่] --> Q1{ชื่อสื่อเจตนาหรือไม่}
    Q1 -->|ไม่| FixNaming[ปรับปรุงชื่อตัวแปรและ function]
    Q1 -->|ใช่| Q2{function ทำหลายอย่างหรือไม่}
    FixNaming --> Q2
    Q2 -->|ใช่| ExtractMethod[แยกออกเป็น function ย่อยที่มี cohesion สูง]
    Q2 -->|ไม่| Q3{มี conditional ซ้อนลึกหรือไม่}
    ExtractMethod --> Q3
    Q3 -->|ใช่| GuardClause[ใช้ guard clause หรือ polymorphism]
    Q3 -->|ไม่| Q4{ธรรมเนียมการจัดรูปแบบสม่ำเสมอหรือไม่}
    GuardClause --> Q4
    Q4 -->|ไม่| ApplyStyle[รัน formatter และ linter]
    Q4 -->|ใช่| Done[code อ่านง่ายพอสำหรับทีม]
    ApplyStyle --> Done

ตัวอย่าง​ต่อ​ไป​นี้​แสดง method เดียวกัน​สอง​แบบ — แบบ​แรก​อ่าน​ยาก​เพราะ​ชื่อ​คลุมเครือ เงื่อนไข​ซ้อน​ลึก และ​ผสม​หลาย​ระดับ​ความคิด​ไว้​ด้วย​กัน ส่วน​แบบ​ที่​สอง​ปรับปรุง​ด้วย​การ​ตั้ง​ชื่อ​สื่อ​เจตนา guard clause และ​การ​แยก function ย่อย:

// ก่อนปรับปรุง: อ่านยาก
public decimal Calc(Order o)
{
decimal t = 0;
if (o != null)
{
if (o.Items != null && o.Items.Count > 0)
{
foreach (var i in o.Items)
{
if (i.Qty > 0)
{
if (i.Discount > 0)
{
t += i.Price * i.Qty * (1 - i.Discount);
}
else
{
t += i.Price * i.Qty;
}
}
}
}
}
return t;
}
// หลังปรับปรุง: ชื่อสื่อเจตนา, guard clause, แยกความรับผิดชอบ
public decimal CalculateOrderTotal(Order order)
{
if (order?.Items is null || order.Items.Count == 0)
{
return 0m;
}
return order.Items
.Where(item => item.Quantity > 0)
.Sum(CalculateLineTotal);
}
private decimal CalculateLineTotal(OrderItem item)
{
var discountMultiplier = 1 - item.Discount;
return item.Price * item.Quantity * discountMultiplier;
}

version ที่​สอง​อ่าน​ออก​เสียง​เป็น​ภาษา​ไทย​ได้​เกือบ​เป็น​ประโยค: “คำนวณ​ยอด​รวม​ของออเดอร์​จาก​รายการ​สินค้า​ที่​มี​จำนวน​มากกว่า​ศูนย์” ผู้​อ่าน​ไม่​ต้อง​นั่ง​ไล่​ตาม​เงื่อนไข​ซ้อน​กัน​สาม​ชั้น​เพื่อ​รู้​ว่า method นี้​ทำ​อะไร และ​หาก​ต้อง​แก้ไข​สูตร​คำนวณ​ส่วนลด​ใน​อนาคต ก็​รู้ทันที​ว่า​ต้อง​แก้​ที่ CalculateLineTotal เพียง​จุด​เดียว

ประโยชน์

  • ลด​ต้นทุน​การ​ดูแล​รักษา — เพราะ code ถูก​อ่าน​บ่อย​กว่า​ถูก​เขียน​หลาย​เท่า การ​ลงทุน​เวลา​ให้ code อ่าน​ง่าย​วัน​นี้​คือ​การ​ประหยัด​เวลา​ของ​ทั้ง​ทีม​ใน​อนาคต
  • Code review เร็ว​ขึ้น​และ​มี​คุณภาพ​ขึ้น — ผู้​ทวน code ใช้​เวลา​น้อย​ลง​ใน​การ​ถอดรหัส code และ​มี​เวลา​เหลือ​ไป​โฟกัส​กับ logic หรือ design จริง ๆ
  • ลด​ความ​เสี่ยง​ของ bug — code ที่​อ่าน​ง่าย​ทำให้​เห็น edge case และ​ข้อ​ผิดพลาด​ได้​ชัดเจน​กว่า ทั้ง​ตอน​เขียน​และ​ตอน​ทวน
  • Onboarding นัก​พัฒนา​ใหม่​เร็ว​ขึ้น — codebase ที่​อ่าน​ง่าย​ลด​เวลา​ที่​คน​ใหม่​ต้อง​ใช้​ใน​การ​ทำความ​เข้าใจ​ระบบ​ก่อน​จะ​เริ่ม​มี​ส่วนร่วม​ได้​จริง

ข้อ​ควร​ระวัง

  • อย่า​สับสน​ระหว่าง “สั้น” กับ “อ่าน​ง่าย” — การ​แยก function ย่อย​มาก​เกิน​ไป​จน​ทุก method เรียก​ไป​ยัง method เล็ก ๆ อีก​ตัว​หนึ่ง (อาจ​อยู่​ใน​อีก class) อาจ​ทำให้​ต้อง​ไล่​ตาม​เส้น​ทางการ​ทำงาน​ผ่าน class เป็น​สิบ ๆ ตัว​เพื่อ​ดู​ว่า method ง่าย ๆ ตัว​หนึ่ง​ทำ​อะไร​กัน​แน่ — นี่​คือ​การ​แลก​ความ​อ่าน​ง่าย​ใน​ระดับ method เดียว​กับ​ความ​อ่าน​ง่าย​ใน​ระดับ​ภาพ​รวม​ของ​ระบบ ต้องหา​จุด​สมดุล
  • Readability ไม่ใช่​รสนิยม​ส่วนตัว — ทีม​ควร​ตกลง​ธรรมเนียม​ร่วม​กัน (coding conventions) และ​บังคับ​ใช้​ด้วย​เครื่องมือ ไม่ใช่​ปล่อย​ให้​แต่ละ​คน​เขียน​ตาม​สไตล์​ตัวเอง เพราะ​ความ​ไม่​สม่ำเสมอ​เอง​ก็​ทำลาย​ความ​อ่าน​ง่าย​ได้
  • อย่า​ใช้​คอมเมนต์​ปิดบัง code ที่​แย่ — ถ้า​ต้อง​เขียน​คอมเมนต์​ยาว​เพื่อ​อธิบาย​ว่า code ทำ​อะไร นั่น​มัก​เป็น​สัญญาณ​ว่า​ควร​ปรับปรุง​ชื่อ​และ​โครงสร้าง code แทนที่​จะ​เพิ่ม​คอมเมนต์
  • Minify/obfuscate ได้​ตอน deploy เท่านั้น — source ที่​ทีม​ทำงาน​ด้วย​จริง​ต้อง​คง​ความ​อ่าน​ง่าย​ไว้​เสมอ ส่วน​การ​บีบ​อัด​หรือ​ทำให้​อ่าน​ยาก​เป็น​เรื่อง​ของ build pipeline ไม่ใช่ source of truth