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

Practices from Code That Fits in Your Head

ชุด​แนว​ปฏิบัติ​จาก​หนังสือ​ของ Mark Seemann — แผนที่​อ่าน​ต่อ

Code That Fits in Your Head: Heuristics for Software Engineering (2021) เป็น​หนังสือ​ของ Mark Seemann ผู้​เขียน blog ชื่อ​ดัง ploeh.dk และ​เป็น​หนึ่ง​ใน​ผู้​เขียน​ร่วม​ของ​ซีรีส์ Robert C. Martin หนังสือ​เล่ม​นี้​ไม่​ได้​เสนอ​ทฤษฎี​ใหม่ แต่​รวบรวม heuristics (แนวทาง​ที่​ใช้ได้​ผล​บ่อย​แต่​ไม่ใช่​กฎ​ตายตัว) กว่า​สอง​โหล​ที่ Seemann สั่งสม​มา​จาก​ประสบการณ์​หลาย​สิบ​ปี โดย​ผูก​ทุก​แนว​ปฏิบัติ​เข้า​กับ​แก่น​เดียวกัน — งาน​วิจัย​ด้าน​วิทยาศาสตร์​การ​รู้คิด (cognitive science) ที่​ชี้​ว่า​สมอง​มนุษย์​เก็บ​สิ่งของ​ใน​ความ​จำ​ระยะ​สั้น (working memory) ได้​ราว​เจ็ด​ชิ้น​เท่านั้น (แนวคิด “the magical number seven” ของ George Miller) ถ้า code หนึ่ง​จุด​ต้อง​ให้​ผู้​อ่าน​ถือ​รายละเอียด​ไว้​ใน​หัว​มากกว่า​นั้น ความ​เข้าใจ​จะ​เริ่ม​พัง

ชื่อ​หนังสือ​จึง​เป็น​ทั้ง​ชื่อ​เล่ม​และ​หลักการ นั่น​คือ code ควร​พอดี​กับ​หัว — ไม่ใช่​แค่​ของ​คน​เขียน แต่​ของ​ทุก​คน​ที่​ต้อง​อ่าน code นั้น​ต่อ​ใน​อนาคต หนังสือ​พา​ผู้​อ่าน​ไป​ตั้งแต่​ยัง​ไม่มี code สัก​บรรทัด ผ่าน project ตัวอย่าง​ภาษา C# ที่​มี commit นับ​ร้อย​ครั้ง จนถึง​การ deploy feature จริง ครอบคลุม​ทั้ง​การ​เขียน code การ​ออกแบบ API การ refactor สถาปัตยกรรม และ​การ​ทำงาน​เป็น​ทีม

DevIQ รวบรวม​แนว​ปฏิบัติ​เหล่า​นี้​ไว้​เป็น​ดัชนี​เดียว เพราะ​แต่ละ​หัวข้อ​สั้น​เกิน​กว่า​จะ​แยก​เป็น​บทความ​เดี่ยว หน้า​นี้​จึง​ทำ​หน้าที่​เป็น แผนที่: สรุป​กลุ่ม​แนว​ปฏิบัติ​หลัก พร้อม link ไป​ยัง​หัวข้อ​ที่​มี​อยู่​แล้ว​ใน​คู่มือ​เล่ม​นี้

แนว​ปฏิบัติ​ใน​หนังสือ​แบ่ง​ได้​เป็นกลุ่ม​ใหญ่ ๆ ดังนี้

Cyclomatic Complexity Limit — นับ​จำนวน​เส้นทาง (path) ที่​เป็น​ไป​ได้​ผ่าน code 1 method ทุก if, for, while, case, หรือ operator แบบ short-circuit เพิ่ม​เส้นทาง​อย่าง​น้อยหนึ่ง​เส้น Seemann แนะนำ​ให้​ทีม​ตกลง​เพดาน​ร่วม​กัน (เขา​ใช้ 7 เป็น​ค่า​เริ่มต้น ตาม​จำนวน​สิ่ง​ที่ working memory รับ​ไหว) ถ้า method ใด​เกิน​เพดาน ให้​แตก​ออก​เป็น method ย่อย

Count the Variables — วัด​ความ​ซับซ้อน​อีก​มุม​หนึ่ง​ด้วย​การ​นับ parameter บวก local variable บวก field ที่ method หนึ่ง​แตะ​ถึง ยิ่ง​ตัวแปร​ที่​ต้อง “ถือ​ไว้​ใน​หัว” พร้อม​กัน​มาก​เท่า​ไหร่ ยิ่ง​อ่าน​ยาก

The 80/24 Rule และ The 50/72 Rule — จำกัด​ความ​กว้าง/สูง​ของ code และ​ข้อความ commit ให้​พอดี​กับ​หน้า​จอ terminal มาตรฐาน (80 column × 24 บรรทัด) และ​พอดี​กับ​บรรทัด​หัวเรื่อง/เนื้อหา​ของ commit message (50/72 ตัว​อักษร) เพื่อ​ไม่​ให้​ต้อง scroll หรือ wrap ขณะ​อ่าน

Checklist for a New Code Base — รายการ​ตั้ง​ค่าที่​ควร​ทำ​ตั้งแต่​วัน​แรก​ของ repository เช่น เปลี่ยน compiler/linter warning ให้​เป็น error, ตั้ง continuous integration, ทำให้ test suite รัน​จบ​ภายใน​ไม่​กี่​วินาที และ commit ทุก​ครั้ง​ที่ build ผ่าน เป้าหมาย​คือ​ให้​สภาพ​แวดล้อม​พัฒนา​สร้าง feedback loop ที่​สั้น​และ​เชื่อถือ​ได้​ตั้งแต่​วัน​แรก ก่อน​ที่ code จะ​เยอะ​จน​แก้​ปัญหา​พื้นฐาน​เหล่า​นี้​ย้อนหลัง​ยาก

Semantic Versioning · Feature Flag · Regularly Update Dependencies — สาม​แนว​ปฏิบัติ​ที่​ทำให้การ​ส่ง​มอบ​ซอฟต์แวร์​เป็น​จังหวะ​สม่ำเสมอ ไม่ใช่​เหตุการณ์​ใหญ่​ที่​น่า​กลัว — version สื่อ​ความหมาย​ชัดเจน feature ใหม่​ซ่อน​หลัง flag จน​พร้อม และ dependency ไม่​ถูก​ปล่อย​ให้​ล้าหลัง​จน​อัปเกรด​ที​เดียว​เป็น​ความ​เสี่ยง​มหาศาล

Slice (vertical) — ตัด​งาน​ตาม feature ที่​ส่ง​มอบ​ได้​จริง (จาก UI ถึง data store) แทนที่​จะ​ตัด​ตาม layer สถาปัตยกรรม ทำให้​แต่ละ commit เพิ่ม​คุณค่า​ที่​ทดสอบ​และ deploy ได้​ทันที

Transformation Priority Premise — เมื่อ​ทำ Red, Green, Refactor ให้​เลือก​การ​แปลง code (transformation) ที่ “เรียบ​ง่าย​ที่สุด” ก่อน​เสมอ เช่น เปลี่ยน​ค่า​คงที่​เป็น​ตัวแปร​ก่อน​เปลี่ยน​เงื่อนไข if เป็น loop เพื่อ​ให้​แต่ละ​ก้าว​ของ TDD เล็ก​และ​คาด​เดา​ได้

Functional Core, Imperative Shell — แยก​ตรรกะ​ทาง​ธุรกิจ​ที่​เป็น pure function (ไม่มี side effect, ทดสอบ​ง่าย) ออก​จาก​เปลือก​บาง ๆ ที่​จัดการ I/O เช่น​ฐาน​ข้อมูล​หรือ​เครือข่าย แก่น​ที่​บริสุทธิ์​ทำให้​ส่วน​ใหญ่​ของ​ระบบ​พอดี​กับ​หัว​ได้​ง่าย​กว่า​มาก เพราะ​ไม่​ต้อง​คิด​พร้อม​กัน​ทั้ง logic และ side effect

Decorators for Cross-Cutting Concerns — ห่อ​พฤติกรรม​ที่​ตัด​ขวาง​หลาย​ชั้น (logging, caching, retry) ด้วย decorator แทน​การ​ฝัง code ซ้ำ ๆ ใน​ทุก class ดู​เพิ่ม​ที่ Cross-Cutting Concerns และ Strangler (Fig) สำหรับ​การ​ค่อย ๆ แทนที่​ระบบ​เก่า​แบบ​เดียวกัน

Reproduce Defects as Tests — ก่อน​แก้ bug ใด ๆ ให้​เขียน test ที่ fail เพื่อ​จำลอง bug นั้น​ก่อน แล้ว​ค่อย​แก้​ให้ test ผ่าน วิธี​นี้​ยืนยัน​ว่า bug ถูก​แก้​จริง​และ​ป้องกัน​การ regress

Devil’s Advocate — ตั้งใจ​โต้แย้ง test ของ​ตัวเอง ลอง​เขียน implementation ที่​ผิด​แต่​ยัง​ผ่าน test ถ้า​ทำได้ แปล​ว่า test ยัง​ไม่​ครอบคลุม​พอ

Bisection — เมื่อ bug ซ่อน​อยู่​ใน​ประวัติ commit จำนวน​มาก ให้​แบ่ง​ครึ่ง​ช่วง​ที่​สงสัย​ไป​เรื่อย ๆ (คล้าย​หลักการ​ของ git bisect) แทน​การ​ไล่​ที​ละ commit

Review Code · Whole Team · Pair Programming — Seemann แนะนำ​ให้ code review ใช้​เวลา​ไม่​เกิน​หนึ่ง​ชั่วโมง​ต่อ​ครั้ง (เพราะ​สมาธิ​ของ​ผู้​ตรวจ​ก็​มี​ขีด​จำกัด​เช่น​กัน) และ​หลีก​เลี่ยง​การ​ให้​ผู้​เขียน code นั่ง​อธิบาย test ระหว่าง​รีวิว เพราะ​จะ​ชักจูง​ผู้​ตรวจ​โดย​ไม่​ตั้งใจ นอกจาก​นี้​ยัง encourage การ integrate code เข้า main branch บ่อย ๆ (เช่น​ทุก​ไม่​กี่​ชั่วโมง) เพื่อ​ลด merge conflict ที่​ซับซ้อน​เกิน​จะ​แก้​ใน​หัว​ได้

ตัวอย่าง​ต่อ​ไป​นี้​แสดง​แนวคิด Count the Variables ร่วม​กับ Cyclomatic Complexity Limit — method ซ้าย​มี​เงื่อนไข​ซ้อน​กัน​หลาย​ชั้น​และ​ตัวแปร​ที่​ต้อง​ติดตาม​พร้อม​กัน​เกิน​เจ็ด จึง​ถูก​แตก​เป็น method ย่อย​ตาม​แนวทาง Functional Core, Imperative Shell:

// ก่อน: cyclomatic complexity สูง ต้องถือตัวแปรหลายตัวไว้ในหัวพร้อมกัน
public decimal CalculateDiscount(Order order, Customer customer, DateTime now)
{
decimal discount = 0m;
if (customer.IsVip)
{
if (order.Total > 1000m)
{
discount = order.Total * 0.15m;
}
else if (order.Total > 500m)
{
discount = order.Total * 0.10m;
}
}
else if (now.Month == 12)
{
discount = order.Total * 0.05m;
}
return discount;
}
// หลัง: แกนคำนวณเป็น pure function ล้วน ๆ (functional core)
// แยกกฎแต่ละข้อออกเป็น method เล็ก ทดสอบและอ่านทีละชิ้นได้
public decimal CalculateDiscount(Order order, Customer customer, DateTime now) =>
customer.IsVip
? VipDiscount(order.Total)
: SeasonalDiscount(order.Total, now);
private static decimal VipDiscount(decimal total) =>
total switch
{
> 1000m => total * 0.15m,
> 500m => total * 0.10m,
_ => 0m
};
private static decimal SeasonalDiscount(decimal total, DateTime now) =>
now.Month == 12 ? total * 0.05m : 0m;

การ​แตก method แบบ​นี้​ไม่​ได้​ลด​จำนวน logic ที่​มี​อยู่​จริง แต่​ลด​จำนวน​สิ่ง​ที่​ต้อง “ถือ​ไว้​ใน​หัว​พร้อม​กัน” ใน​แต่ละ​จุด — ตรง​กับ​ใจความ​หลัก​ของ​หนังสือ​ทั้ง​เล่ม

ประโยชน์

  • แนว​ปฏิบัติ​ทั้งหมด​ผูก​กับ​หลักการ​เดียว​ที่​จำ​ง่าย (“ให้​พอดี​กับ​หัว”) จึง​เป็น​เกณฑ์​ตัดสิน​ใจ​ที่​ใช้ได้​ทั่วไป แม้​จะ​เป็น​สถานการณ์​ที่​หนังสือ​ไม่​ได้​ยก​ตัวอย่าง​ไว้​ตรง ๆ
  • ลด​ภาระ​การ debug และ​การ onboarding เพราะ code test และ commit ถูก​ออกแบบ​ให้​อ่าน​เป็น​ชิ้น​เล็ก​ที่​จบ​ใน​ตัว
  • ครอบคลุม​ตั้งแต่​ระดับ code ถึง​ระดับ​ทีม จึง​ใช้​เป็น checklist คุณภาพ​โดย​รวม​ของ project ได้ ไม่ใช่​แค่​กฎ​การ​เขียน code

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

  • ตัวเลข​อย่าง “เจ็ด” (cyclomatic complexity, จำนวน​ตัวแปร) เป็น heuristic ไม่ใช่​กฎ​ตายตัว — ทีม​ต้อง​ปรับ​ให้​เหมาะ​กับ​บริบท​ของ​ตัวเอง การ​ยึด​ตัวเลข​แข็ง ๆ อาจ​ทำให้​แตก method จน​สูญเสีย cohesion
  • ตัวอย่าง​ทั้ง​เล่ม​เขียน​ด้วย C# และ​แนวคิด​เชิง​วัตถุ บาง​แนว​ปฏิบัติ (เช่น Functional Core, Imperative Shell) ต้อง​ตีความ​เพิ่มเติม​เมื่อ​นำ​ไป​ใช้​กับ​ภาษา​หรือ​สไตล์​อื่น
  • แนว​ปฏิบัติ​ระดับ​ทีม (เช่น review ไม่​เกิน​หนึ่ง​ชั่วโมง, integrate ทุก​ไม่​กี่​ชั่วโมง) ต้อง​ได้​รับ​ความเห็น​ชอบ​ร่วม​กัน​ของ​ทีม​ก่อน ไม่ใช่​สิ่ง​ที่​คน​คน​เดียว​บังคับ​ใช้ได้