มายากลในการเขียนโปรแกรมไม่ใช่คำอุปมา แต่เป็นคำศัพท์ที่แม่นยำซึ่งหมายถึงค่า (ตัวเลข สตริง แฟล็ก) ที่ความหมายไม่ชัดเจนจากบริบทและต้องการความรู้ภายนอกเพื่อทำความเข้าใจ ประเภทมายากลที่พบบ่อยที่สุดคือ magic numbers: ค่าคงที่ตัวเลขที่เขียนลงในโค้ดโดยตรงโดยไม่มีคำอธิบายว่าเหตุใดจึงเลือกค่านั้นโดยเฉพาะ ตามรายงานคุณภาพโค้ดของ SonarSource (2025) ประมาณ 8 เปอร์เซ็นต์ของคำเตือนทั้งหมดจากตัววิเคราะห์แบบคงที่เกี่ยวข้องกับลิเทอรัลที่ไม่มีการอธิบาย ค่ามายากล ทำให้โค้ดเปราะบาง: การเปลี่ยนแปลงต้องค้นหาทุกตำแหน่งที่ปรากฏ และนักพัฒนาใหม่ไม่เข้าใจว่าสามารถแตะต้องตัวเลขได้หรือว่ามันสำคัญต่อการทำงานของระบบ
ประเด็นสำคัญ
มายากล คือค่าใดๆ ในซอร์สโค้ดที่ความหมายไม่ชัดเจนหากไม่มีความรู้เพิ่มเติมในโดเมน คำศัพท์นี้เป็นที่ยอมรับในชุมชน: ถ้านักพัฒนามองดูตัวเลขแล้วบอกไม่ได้ว่ามันมาจากไหน — นั่นคือมายากล
มายากลมีหลายประเภท: ตัวเลข (magic numbers), สตริง (magic strings), บูลีน (magic flags), และการกำหนดค่า (พารามิเตอร์ที่ถูกฮาร์ดโค้ดซึ่งควรอยู่ในตั้งค่า) ทั้งสี่ประเภทมีปัญหาร่วมกัน: เมื่อความต้องการเปลี่ยนแปลง นักพัฒนาต้องค้นหาทุกตำแหน่งที่ใช้ค่าและแทนที่ด้วยตนเอง การพลาดแม้แต่ตำแหน่งเดียวก็นำไปสู่บัก
ตามแบบสำรวจคุณภาพโค้ดของ JetBrains (2025) นักพัฒนา 73 เปอร์เซ็นต์ถือว่า magic numbers เป็นตัวบ่งชี้คุณภาพโค้ดต่ำ ในขณะที่ 41 เปอร์เซ็นต์ยอมรับว่าบางครั้งปล่อยทิ้งไว้ สาเหตุหลักคือความรีบร้อน: “เดี๋ยวจะเพิ่มค่าคงที่ทีหลัง” — แต่ทีหลังนั้นไม่มีวันมา และหนึ่งเดือนต่อมา ตัวเลข 0.85 ก็ยังคงอยู่กลางเมธอดโดยไม่มีคำอธิบาย
กฎสำคัญ: ทุกค่าลิเทอรัลยกเว้น 0, 1, true, false และสตริงว่างควรถูกแยกเป็นค่าคงที่ที่มีชื่อ ข้อยกเว้น: การเพิ่มตัวนับ (i + 1), ศูนย์ทางคณิตศาสตร์ (การตรวจสอบ 0), และค่าเริ่มต้นของตัวสะสม ทุกอย่างอื่นเป็นตัวเลือกสำหรับการตั้งชื่อ
Magic number คือลิเทอรัลตัวเลขที่ค่าไม่ชัดเจนจากบริบท ตัวอย่างคลาสสิก: 86400 ในโค้ดที่เกี่ยวข้องกับหมดเวลา นักพัฒนามองเห็นตัวเลขและต้องเดาว่ามันคือจำนวนวินาทีในหนึ่งวัน ถ้าเขาทำผิดและเขียน 84600 บักจะจับได้ยากเพราะหมดเวลาจะทำงานเร็วกว่า 18 นาที
เหตุใด magic numbers จึงอันตราย: ประการแรก ทำให้อ่านยาก ตัวเลข 1024 อาจหมายถึงขนาดกิโลไบต์ เกณฑ์การแบ่งหน้า หรือจำนวนสูงสุดของรายการ หากไม่มีบริบท — มันก็แค่ตัวเลข ประการที่สอง สร้างความซ้ำซ้อน: ถ้า 1024 ใช้ในห้าตำแหน่ง เมื่อเกณฑ์เปลี่ยนเป็น 2048 นักพัฒนาต้องค้นหาทั้งห้าและแทนที่ หากพลาดตำแหน่งเดียว ระบบทำงานผิดพลาดแต่ไม่มีข้อผิดพลาดชัดเจน
// before - magic in its pure form
fun calculateTimeout(base: Int): Int {
return base * 3 + 5000
}
// after - values replaced with constants
private const val RETRY_MULTIPLIER = 3
private const val BASE_TIMEOUT_MS = 5000
fun calculateTimeout(base: Int): Int {
return base * RETRY_MULTIPLIER + BASE_TIMEOUT_MS
}
อันตรายประการที่สามคือ ไม่สามารถทดสอบได้ ถ้าค่าเกณฑ์ถูกฮาร์ดโค้ดเป็นลิเทอรัล การทดสอบไม่สามารถแทนที่มันเพื่อตรวจสอบเงื่อนไขขอบเขต ค่าคงที่ที่ถูกแยกไปยัง companion object หรือไฟล์กำหนดค่าทำให้โค้ดสามารถทดสอบได้: การทดสอบแทนที่ค่าอื่นและตรวจสอบพฤติกรรมของระบบที่ขอบเขต
พัฒนานิสัย: ทุกครั้งที่คุณเขียนตัวเลขที่ไม่ใช่ 0, 1, 100 หรือ 2 — หยุดและพิจารณาว่าควรแยกเป็นค่าคงที่หรือไม่ ถ้าตัวเลขเกี่ยวข้องกับตรรกะทางธุรกิจ (ขีดจำกัด เกณฑ์ หมดเวลา ขนาด) — แยกโดยไม่ลังเล ถ้าตัวเลขเป็นค่าคงที่ทางคณิตศาสตร์ (pi, e) — ใช้ไลบรารีมาตรฐาน (Math.PI, Math.E)
สตริงมายากล คือลิเทอรัลสตริงที่ถูกฝังในโค้ดโดยไม่ถูกแยกเป็นค่าคงที่หรือทรัพยากร ตัวอย่างทั่วไป: URL ปลายทาง, ชื่อคีย์ SharedPreferences, Intent Actions, คีย์ bundle, ชื่อไฟล์ และคำสั่ง SQL
อันตรายของสตริงมายากลคือ ขาดการตรวจสอบในเวลาคอมไพล์ การพิมพ์ผิดในสตริง “user_prefs” จะไม่ถูกตรวจพบจนกว่าจะถึงเวลาทำงาน ถ้าสตริงถูกใช้ในสิบตำแหน่งและนักพัฒนาเขียน “user_pref” (ไม่มี s) ในตำแหน่งใดตำแหน่งหนึ่ง — แอปไม่หยุดทำงาน แต่ข้อมูลไม่ถูกบันทึก บักดังกล่าวสามารถอยู่ในโปรดักชันได้เป็นเดือนเพราะมันไม่ทำให้เกิดการหยุดทำงาน
สำหรับโปรเจกต์ Android สตริงมายากลควรถูกแยกเป็นทรัพยากร (strings.xml, arrays.xml) หรือค่าคงที่ใน companion object สำหรับ iOS — เป็นทรัพยากรสตริง (Localizable.strings) หรือค่าคงที่ enum สำหรับแบ็กเอนด์ — เป็นไฟล์กำหนดค่า (.env, application.properties) ไม่มีคีย์ URL หรือพาทใดควรปรากฏในโค้ดเป็นลิเทอรัลสตริง
// before - magic strings across the class
let prefs = UserDefaults.standard
prefs.set(token, forKey: "auth_token")
prefs.set(userId, forKey: "current_user_id")
// after - strings extracted to enum
enum PrefKeys: String {
case authToken = "auth_token"
case currentUserId = "current_user_id"
}
prefs.set(token, forKey: PrefKeys.authToken.rawValue)
prefs.set(userId, forKey: PrefKeys.currentUserId.rawValue)
ให้ความสนใจเป็นพิเศษกับสตริงที่ซ้ำกัน ถ้าคีย์เดียวกัน “user_settings” ปรากฏในสามไฟล์ — 99 เปอร์เซ็นต์ว่าในที่สุดจะมีการพิมพ์ผิดในหนึ่งในนั้น การแยกเป็น enum หรือค่าคงที่รับประกันว่าการอ้างอิงทั้งหมดใช้ค่าเดียวกัน
Magic flags คือพารามิเตอร์บูลีนที่ความหมายไม่ชัดเจนจากบริบทการเรียกใช้ แอนตี้แพทเทิร์นคลาสสิก: การส่ง true หรือ false ไปยังเมธอดโดยไม่อธิบายว่าแฟล็กนั้นเปิดหรือปิดอะไร
ตัวอย่าง: userDao.fetch(includeDeleted = false) นักพัฒนาเห็น false และบอกไม่ได้ว่ามันหมายถึง “ไม่รวมที่ถูกลบ” หรือ “ไม่รวมที่ใช้งานอยู่” หนึ่งเดือนต่อมา false กลายเป็น true และเรกคอร์ดที่ถูกลบเริ่มปรากฏในผลลัพธ์ บักถูกค้นพบในโปรดักชันเท่านั้น
วิธีแก้คือแทนที่แฟล็กบูลีนด้วย enum หรือคลาส sealed แทนพารามิเตอร์ Boolean ให้ใช้ UserFilter.includeDeleted หรือ UserFilter.activeOnly วิธีนี้โค้ดจะบันทึกเจตนาของมัน และ IDE แนะนำตัวเลือกที่มีระหว่างการเติมข้อความอัตโนมัติ
ถ้าแฟล็กบูลีนถูกส่งผ่านหลายชั้น — นั่นเป็นสัญญาณอีกอย่างว่าแอบสแตรกชันผิด แทนที่จะลากแฟล็กผ่านสามระดับการเรียกใช้ ให้พิจารณาว่าควรเลือกตัวกรองที่ระดับบนสุดและส่งเป็นการกำหนดค่าพร้อมใช้หรือไม่ ยิ่งมีแฟล็กบูลีนในโค้ดน้อยเท่าไร — ก็ยิ่งมีมายากลน้อยลง
ใช้กฎ: ไม่มีพารามิเตอร์บูลีนใดถูกส่งไปยังเมธอดโดยไม่มีอาร์กิวเมนต์ที่มีชื่อ (ถ้าภาษารองรับอาร์กิวเมนต์ที่มีชื่อ) ใน Kotlin และ Swift ข้อกำหนดนี้เป็นไปโดยอัตโนมัติ ใน Java ใช้ Builder หรือค่าคงที่ enum แทน true/false
การค้นหาค่ามายากลเป็นระบบอัตโนมัติโดยตัววิเคราะห์แบบคงที่ที่ถูกกำหนดค่าให้ตรวจจับลิเทอรัลในตำแหน่งที่ไม่คาดคิด แต่ละภาษามีเครื่องมือของตัวเองพร้อมข้อยกเว้นที่ปรับแต่งได้
| เครื่องมือ | ภาษา | กฎ |
|---|---|---|
| SonarQube | Java, Kotlin, Swift, Python, JS | MagicNumber, HardcodedString |
| ESLint | JavaScript, TypeScript | no-magic-numbers, no-hardcoded-strings |
| Detekt | Kotlin | MagicNumber, ComplexCondition |
| SwiftLint | Swift | magic_number (opt-in) |
| PMD | Java, Apex, PLSQL | MagicNumber (รายการที่อนุญาตปรับแต่งได้) |
| การตรวจสอบ PhpStorm | PHP | NumericLiteralWithContext (การตรวจสอบในตัว) |
การกำหนดค่าข้อยกเว้นเป็นสิ่งสำคัญ — หากไม่มี ตัววิเคราะห์จะแจ้งเตือนทุกการเพิ่ม (-1, +1) และศูนย์ทางคณิตศาสตร์ สำหรับ SonarQube รายการตัวเลขที่อนุญาต: 0, 1, -1, 2 (สำหรับการเพิ่มเป็นสองเท่า), 100 (เปอร์เซ็นต์), 60 และ 24 (เวลา) สำหรับค่าอื่นทั้งหมด — กำหนดให้มีค่าคงที่ที่มีชื่อพร้อมตัวปรับ public static final (Java) หรือ const val (Kotlin)
สำหรับการวิเคราะห์ระดับ CI ให้เพิ่มขั้นตอนที่ตรวจสอบมายากลเป็นคำเตือนแต่ไม่บล็อกบิลด์ การรันครั้งแรกจะแสดงคำเตือนหลายร้อยรายการในโค้ดเก่า ค่อยๆ ทีละ ticket ย้ายโค้ดไปใช้ค่าคงที่และเพิ่มเกณฑ์คุณภาพ เมื่อจำนวน magic numbers ต่ำกว่า 10 — เปิดกฎเป็นข้อผิดพลาดในการบิลด์
รีแฟกเตอริงมายากลเป็นหนึ่งในการดำเนินการที่ปลอดภัยที่สุด: การแทนที่ลิเทอรัลด้วยค่าคงที่ไม่เปลี่ยนพฤติกรรมของโค้ด อย่างไรก็ตาม วิธีการต้องเป็นระบบเพื่อไม่ให้มองข้ามการพึ่งพาที่ซ่อนอยู่ (เช่น ถ้า magic number เดียวกันถูกใช้ในบริบทที่ไม่เกี่ยวข้องแต่บังเอิญมีค่าเท่ากัน)
กระบวนการทีละขั้นตอน: ค้นหาทุกตำแหน่งของค่ามายากล เข้าใจบริบทของแต่ละตำแหน่ง แยกเป็นค่าคงที่ต่างกัน (แม้ว่าค่าจะตรงกัน — บริบทต่างกัน และค่าคงที่ควรมีชื่อต่างกัน) แทนที่ลิเทอรัลด้วยค่าคงที่ ตรวจสอบผ่านการทดสอบ ข้อผิดพลาดในขั้นตอนที่ 2 พบบ่อยที่สุด: สองแนวคิดที่แตกต่างกัน (หมดเวลาในหน่วยมิลลิวินาทีและเกณฑ์ในหน่วยไบต์) อาจตรงกันเป็นตัวเลข (เช่น 5000) แต่ในเชิงความหมายแล้วเป็นปริมาณที่แตกต่างกันและไม่สามารถรวมเป็นค่าคงที่เดียวได้
// before - same number in different contexts
public class Config {
public void setupCache() {
cache.setMaxSize(5000); // 5 MB
}
public void setupTimeout() {
client.setReadTimeout(5000); // 5 seconds
}
}
// after - different constants for different contexts
public class Config {
private static final int CACHE_MAX_SIZE_MB = 5;
private static final int READ_TIMEOUT_SECONDS = 5;
public void setupCache() {
cache.setMaxSize(CACHE_MAX_SIZE_MB * 1024 * 1024);
}
public void setupTimeout() {
client.setReadTimeout(
READ_TIMEOUT_SECONDS * 1000
);
}
}
สำหรับโค้ดใหม่ กฎง่ายๆ: ลิเทอรัลใดๆ ยกเว้น 0, 1, -1, true, false, null และสตริงว่างจะถูกแยกเป็นค่าคงที่ ข้อยกเว้น: ค่าคงที่ทางคณิตศาสตร์ (ใช้ไลบรารีมาตรฐานเสมอ), ข้อมูลทดสอบ (ลิเทอรัลสามารถอยู่ในทดสอบได้แต่ต้องมีชื่อตัวแปรที่อธิบายความหมาย), และค่าขอบเขตสำหรับการเพิ่ม (i + 1 ในลูปไม่เป็นไร)
คำถามที่พบบ่อย
ใช่ 100 ก็เป็น magic number ถ้าใช้โดยไม่มีบริบท แทนที่ 100 ให้เขียน MAX_PERCENT หรือ PROBABILITY_SCALE ข้อยกเว้น: เมื่อ 100 เป็นเปอร์เซ็นต์ที่ชัดเจนในบริบท (เช่น ในสูตรคำนวณเปอร์เซ็นต์) แต่แม้ในกรณีนี้ค่าคงที่ก็ช่วยให้อ่านง่ายขึ้น
ในการทดสอบก็ควรใช้ตัวแปรที่มีชื่อเช่นกัน แทนที่ assertEquals(42, result) ให้เขียน val expected = 42; assertEquals(expected, result) ข้อยกเว้น: การทดสอบค่าขอบเขต (0, null, สตริงว่าง) — สามารถปล่อยเป็นลิเทอรัลได้เพราะอ่านเข้าใจได้ในบริบทของการทดสอบ
ใช่ ตัวเลขที่เกี่ยวข้องกับ UI (ขนาด ระยะขอบ ระยะเวลาแอนิเมชัน) ควรอยู่ในทรัพยากร (dimens.xml, integers.xml) ค่าคงที่ทางธุรกิจ (หมดเวลา ขีดจำกัด) — ใน companion object หรือไฟล์กำหนดค่า เกณฑ์หลัก: ถ้าตัวเลขสามารถเปลี่ยนได้โดยไม่เปลี่ยนตรรกะ — มันคือทรัพยากร
รัน SonarQube ด้วยกฎ MagicNumber หรือ ESLint ด้วย no-magic-numbers รับรายงาน เรียงตามความถี่ในการใช้งาน และเริ่มด้วยตัวเลขที่ปรากฏในสามตำแหน่งขึ้นไป มันเป็นตัวเลือกที่มีแนวโน้มมากที่สุดสำหรับการแยกเป็นค่าคงที่
ไม่ ลิเทอรัลที่ยอมรับได้: 0, 1, -1 (เพิ่ม/ลด, ตรวจสอบว่าง), true, false, null, สตริงว่าง ที่เหลือทั้งหมดต้องการการตั้งชื่อ ถ้าตัวเลข 0 ไม่ได้ใช้เป็นการตรวจสอบว่าง (เช่น 0 คือ ID หมวดหมู่ราก) แล้ว 0 ก็ควรเป็นค่าคงที่: ROOT_CATEGORY_ID = 0
สรุป
เราจะพัฒนาแอปพลิเคชันบนมือถือแบบครบวงจร
IT Sectr สร้างแอปพลิเคชัน iOS และ Android สำหรับสตาร์ทอัพและธุรกิจตั้งแต่ปี 2017 เราจะให้คำแนะนำและเสนอวิธีแก้ปัญหาที่ดีที่สุดแก่คุณ
อ่านเพิ่มเติม