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 สื่อเจตนาได้ด้วยตัวมันเอง
ตัวอย่างและการ refactor
หัวข้อที่มีชื่อว่า “ตัวอย่างและการ refactor”ตัวอย่างที่ 1 — นิพจน์คำนวณที่บีบรวมกัน
หัวข้อที่มีชื่อว่า “ตัวอย่างที่ 1 — นิพจน์คำนวณที่บีบรวมกัน”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 Variablepublic 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