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

Obscured Intent

code ที่​เขียน​จน​มอง​ไม่​ออกว่า​กำลัง​พยายาม​ทำ​อะไร

Obscured Intent เป็น​คำ​ที่ Robert C. Martin ใช้​ใน Clean Code (2008) เรียก code ที่ ทำงาน​ถูกต้อง​ทาง​เทคนิค แต่​ไม่​สื่อ​ความหมาย​ว่า​กำลัง​ทำ​อะไร​หรือ​ทำไม ผู้​อ่าน​ต้อง​นั่ง​ไล่​ที​ละ​บรรทัด แปล​ตัวแปร​ใน​หัว แล้ว​ค่อย​ประกอบ​ภาพ​รวม​เอง ก่อน​จะ​เข้าใจ​สิ่ง​ที่ code ตั้งใจ​จะ​ทำ

ใน​ทาง​อนุกรม​วิธาน​ของ​กลิ่น code (ตาม​ที่ Luzkan รวบรวม​ไว้) Obscured Intent ถูก​จัด​อยู่​ใน​กลุ่ม “Obfuscators” — กลิ่น​ที่​ทำให้ code อ่าน​ยากกว่าที่​ควร​จะ​เป็น โดย​ไม่​ได้​เกี่ยว​กับ​โครงสร้าง class หรือ​การ​พึ่งพา​โดยตรง แต่​เป็น​เรื่อง​ของ “ความ​ซับซ้อน​ที่​ไม่​จำเป็น” (unnecessary complexity) ล้วน ๆ

จุด​สำคัญ​คือ Obscured Intent มัก​ไม่ใช่​กลิ่น​เดี่ยว ๆ แต่​เป็น ผล​รวม ของ​กลิ่น​อื่น​ที่มา​ซ้อน​กัน เช่น Poor Names, ตัวเลข​ปริศนา (magic numbers), นิพจน์บู​ลีน​ซับซ้อน, หรือ​คอมเมนต์​ที่​พยายาม​อธิบาย “อะไร” (what) แทนที่​จะ​ปล่อย​ให้ code พูด​เอง เมื่อ​กลิ่น​เหล่า​นี้​มา​ซ้อน​กัน​หลาย​ชั้น ผลลัพธ์​คือ code ที่​เจตนา​ถูก “บดบัง” จน​มอง​ไม่​เห็น

  • ต้อง​อ่าน code ซ้ำ​สอง​สาม​รอบ หรือ​รัน​ใน​หัว​ที​ละ​สเต็ป ถึง​จะ​เข้าใจ​ว่า​มัน​ทำ​อะไร
  • ชื่อ​ตัวแปร/parameter/method สั้น​ห้วน​หรือ​ย่อ​จน​เดา​ความหมาย​ไม่​ออก (เช่น h, r, tmp, flag2)
  • นิพจน์​คณิตศาสตร์​หรือ​เงื่อนไข​บู​ลีน​หลาย​เงื่อนไข​ถูก​อัด​ไว้​ใน​บรรทัด​เดียว ไม่มี​การ​แยก​ส่วน​ย่อย
  • มี​ตัวเลข​หรือ​ค่า​คงที่​ปริศนา​ที่​ไม่มี​คำ​อธิบาย (magic numbers) ปน​อยู่​ใน​นิพจน์
  • มี​คอมเมนต์​ยาว ๆ ที่​ต้อง​อธิบาย​ว่า code ข้าง​ล่าง “กำลัง​ทำ​อะไร” — สัญญาณ​ว่า code ควร​พูด​แทน​คอมเมนต์​ได้​เอง
  • code ถูก​บีบ​ให้​กระชับ​หรือ “ฉลาด” เกิน​ความ​จำเป็น (clever code) เพื่อ​ความ​เท่​หรือ​ประหยัด​บรรทัด แลก​กับ​ความ​อ่าน​ง่าย
  • รีวิว code แล้ว​เพื่อน​ร่วม​ทีม​ต้อง​ถาม​ว่า “บรรทัด​นี้​มัน​ทำ​อะไร​กัน​แน่”

เจตนา​ที่​มอง​ไม่​เห็น​แปล​ว่า ทุก​คน​ที่​ต้อง​แตะ code นี้​ใน​อนาคต (รวม​ถึง​ตัว​เรา​เอง​ใน​อีก​หก​เดือน) ต้อง​เสีย​เวลา​แกะ​รหัส​ก่อน​จะ​แก้ไข​อะไร​ได้​จริง นั่น​คือ​ต้นทุน​ที่​มอง​ไม่​เห็น​แต่​จ่าย​ซ้ำ​ทุก​ครั้ง​ที่​มี​คน​เข้า​มา​อ่าน

ยิ่ง​ไป​กว่า​นั้น เมื่อ​ไม่​เข้าใจ​เจตนา​เดิม การ​แก้ไข​หรือ​เพิ่ม feature ก็​มี​ความ​เสี่ยง​สูง​ที่​จะ​ทำ​ผิดพลาด เพราะ​แก้​ตาม “สิ่ง​ที่ code ดูเหมือน​จะ​ทำ” แทนที่​จะ​แก้​ตาม “สิ่ง​ที่ code ควร​ทำ​จริง ๆ” — ความ​เสี่ยง​นี้​ทวีคูณ​เมื่อ code นั้น​อยู่​ใน​เส้นทาง​ธุรกิจ​สำคัญ เช่น การ​คำนวณ​เงิน​หรือ​สิทธิ์​การ​เข้าถึง

เนื่องจาก Obscured Intent เป็น​ผล​รวม​ของ​กลิ่น​ย่อย​หลาย​ตัว การ​แก้ไข​ที่​ผิวเผิน (เช่น​แค่​เติม​คอมเมนต์) จะ​ไม่​ช่วย​อะไร — คอมเมนต์​กลาย​เป็น​แค่ “น้ำหอม​กลบ​กลิ่น” ทาง​ที่​ถูกต้อง​คือ​ต้อง​ไล่​กำจัด​กลิ่น​ย่อย​ที่​ต้นตอ (ชื่อ​แย่ method ยาว นิพจน์​ซับซ้อน) จน code สื่อ​เจตนา​ได้​ด้วย​ตัว​มัน​เอง

code คำนวณ​ค่า​จ้าง​ล่วง​เวลา​ต่อ​ไป​นี้​ถูกต้อง​ทาง​คณิตศาสตร์ แต่​ชื่อ parameter สั้น​ห้วน​และ​นิพจน์​ถูก​อัด​ไว้​บรรทัด​เดียว ทำให้​ต้อง​นั่ง​ถอด​สมการ​ก่อน​จะ​รู้​ว่า​มัน​คำนวณ​อะไร

// สับสน: ชื่อ parameter ย่อ นิพจน์ซ้อนกันในบรรทัดเดียว ไม่มีตัวแปรอธิบาย
public int CalcPay(int h, decimal r)
{
return (int)(h * r + Math.Round(0.5m * r * Math.Max(0, h - 400)));
}

หลัง refactor ด้วย Rename Method/Variable (ตั้ง​ชื่อ​ให้​สื่อ​เจตนา) ร่วม​กับ Extract Variable (แยก​นิพจน์​ย่อย​ออก​มา​ตั้ง​ชื่อ) เจตนา​ของ code ก็​อ่าน​ออก​ได้​ทันที​โดย​ไม่​ต้อง​มี​คอมเมนต์​เพิ่ม

// ชัดเจน: ชื่อสื่อเจตนา + แยกนิพจน์ย่อยด้วย Extract Variable
public int CalculateWeeklyPay(int hoursWorked, decimal hourlyRate)
{
const int standardHoursPerWeek = 400;
var regularPay = hoursWorked * hourlyRate;
var overtimeHours = Math.Max(0, hoursWorked - standardHoursPerWeek);
var overtimePay = Math.Round(0.5m * hourlyRate * overtimeHours);
return (int)(regularPay + overtimePay);
}

ตัวอย่าง​ที่ 2 — เงื่อนไข​บู​ลีน​ซับซ้อน​ที่​ต้อง​พึ่ง​คอมเมนต์

หัวข้อ​ที่​มีชื่อ​ว่า “ตัวอย่าง​ที่ 2 — เงื่อนไข​บู​ลีน​ซับซ้อน​ที่​ต้อง​พึ่ง​คอมเมนต์”

ตัวอย่าง​คลาสสิกอีก​แบบ​คือ​เงื่อนไข​ที่​มี​ตัว​ดำเนิน​การ​ตรรกะ​หลาย​ตัว​ปน​กัน จน​ต้อง​เติม​คอมเมนต์​อธิบาย “อะไร” เพื่อ​ชดเชย​ความ​อ่าน​ยาก

// สับสน: ต้องมีคอมเมนต์อธิบายว่านิพจน์นี้ทำอะไร เพราะตัว code เองสื่อเจตนาไม่ได้
// เช็คว่าผู้ใช้มีสิทธิ์เข้าถึงหรือไม่
if ((user.Age >= 18 && user.Country == "TH" || user.IsVerified)
&& !user.IsBanned && user.SubscriptionLevel > 0)
{
GrantAccess();
}

ใช้ Extract Variable แยก​แต่ละ​เงื่อนไข​ย่อย​ออก​มา​เป็น​ตัวแปร bool ที่​ตั้ง​ชื่อ​สื่อ​ความหมาย เมื่อ​ทำ​แล้ว​คอมเมนต์​เดิม​ก็​ไม่​จำเป็น​อีก​ต่อ​ไป เพราะ​ชื่อ​ตัวแปร​ทำ​หน้าที่​นั้น​แทน

// ชัดเจน: แต่ละเงื่อนไขมีชื่ออธิบายตัวเอง ไม่ต้องพึ่งคอมเมนต์
var isAdultThaiOrVerified = (user.Age >= 18 && user.Country == "TH") || user.IsVerified;
var hasActiveSubscription = user.SubscriptionLevel > 0;
var isEligible = isAdultThaiOrVerified && !user.IsBanned && hasActiveSubscription;
if (isEligible)
{
GrantAccess();
}

ทั้ง​สอง​ตัวอย่าง​ใช้​หลักการ​เดียวกัน: หา contributing smell ที่แท้​จริง (ชื่อ​แย่ / นิพจน์​ไม่​แยก​ส่วน) แล้ว refactor ที่​ต้นตอ ไม่ใช่​แค่​เติม​คำ​อธิบาย​ทับ​ไว้​ข้าง​บน

flowchart LR
  PoorNames[Poor Names]
  Magic[Magic Numbers]
  CleverCode[Clever Code]
  WhatComments[What Comments]
  ObscuredIntent[Obscured Intent]
  Rename[Rename Method]
  ExtractVar[Extract Variable]
  ExtractMethod[Extract Method]

  PoorNames --> ObscuredIntent
  Magic --> ObscuredIntent
  CleverCode --> ObscuredIntent
  WhatComments --> ObscuredIntent
  ObscuredIntent --> Rename
  ObscuredIntent --> ExtractVar
  ObscuredIntent --> ExtractMethod