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
ที่เกี่ยวข้อง
หัวข้อที่มีชื่อว่า “ที่เกี่ยวข้อง”- Naming Things
- Simple Design
- Comments Code Smell
- Conditional Complexity
- Abstraction Levels Code Smell
- Single Responsibility
แหล่งอ้างอิง
หัวข้อที่มีชื่อว่า “แหล่งอ้างอิง”- ที่มา · deviq.com/practices/code-readability
- Martin Fowler — quote on readable code (Refactoring)
- Coding conventions — Wikipedia
- Naming convention (programming) — Wikipedia
- Create Readable Code with Conventions, Whitespace, and Comments in C# — Microsoft Learn
- Code readability best practices — Power Apps, Microsoft Learn