มายากลในการเขียนโปรแกรม — คืออะไร อันตรายของ magic numbers และวิธีแทนที่

ผู้แต่ง: IT Sectr เผยแพร่เมื่อ: 2026-07-27 เวลาอ่าน: 10 นาที

มายากลในการเขียนโปรแกรมไม่ใช่คำอุปมา แต่เป็นคำศัพท์ที่แม่นยำซึ่งหมายถึงค่า (ตัวเลข สตริง แฟล็ก) ที่ความหมายไม่ชัดเจนจากบริบทและต้องการความรู้ภายนอกเพื่อทำความเข้าใจ ประเภทมายากลที่พบบ่อยที่สุดคือ magic numbers: ค่าคงที่ตัวเลขที่เขียนลงในโค้ดโดยตรงโดยไม่มีคำอธิบายว่าเหตุใดจึงเลือกค่านั้นโดยเฉพาะ ตามรายงานคุณภาพโค้ดของ SonarSource (2025) ประมาณ 8 เปอร์เซ็นต์ของคำเตือนทั้งหมดจากตัววิเคราะห์แบบคงที่เกี่ยวข้องกับลิเทอรัลที่ไม่มีการอธิบาย ค่ามายากล ทำให้โค้ดเปราะบาง: การเปลี่ยนแปลงต้องค้นหาทุกตำแหน่งที่ปรากฏ และนักพัฒนาใหม่ไม่เข้าใจว่าสามารถแตะต้องตัวเลขได้หรือว่ามันสำคัญต่อการทำงานของระบบ

ประเด็นสำคัญ

  • มายากล — ตัวเลข สตริง และแฟล็กโดยนัยในโค้ดที่ความหมายถูกซ่อนจากผู้อ่าน
  • Magic numbers — ลิเทอรัลตัวเลขที่ไม่มีชื่อ: 86400, 3.14, 0.85, 1024
  • สตริงมายากล — พาธ คีย์ URL ที่ถูกฮาร์ดโค้ดโดยไม่แยกเป็นค่าคงที่
  • เครื่องมือตรวจจับ: SonarQube (กฎ MagicNumber), ESLint (no-magic-numbers), Detekt
  • วิธีแก้: แยกทุกค่ามายากลเป็นค่าคงที่ที่มีชื่อพร้อมชื่อที่อธิบายความหมาย

มายากลในการเขียนโปรแกรมคืออะไร?

มายากล คือค่าใดๆ ในซอร์สโค้ดที่ความหมายไม่ชัดเจนหากไม่มีความรู้เพิ่มเติมในโดเมน คำศัพท์นี้เป็นที่ยอมรับในชุมชน: ถ้านักพัฒนามองดูตัวเลขแล้วบอกไม่ได้ว่ามันมาจากไหน — นั่นคือมายากล

มายากลมีหลายประเภท: ตัวเลข (magic numbers), สตริง (magic strings), บูลีน (magic flags), และการกำหนดค่า (พารามิเตอร์ที่ถูกฮาร์ดโค้ดซึ่งควรอยู่ในตั้งค่า) ทั้งสี่ประเภทมีปัญหาร่วมกัน: เมื่อความต้องการเปลี่ยนแปลง นักพัฒนาต้องค้นหาทุกตำแหน่งที่ใช้ค่าและแทนที่ด้วยตนเอง การพลาดแม้แต่ตำแหน่งเดียวก็นำไปสู่บัก

ตามแบบสำรวจคุณภาพโค้ดของ JetBrains (2025) นักพัฒนา 73 เปอร์เซ็นต์ถือว่า magic numbers เป็นตัวบ่งชี้คุณภาพโค้ดต่ำ ในขณะที่ 41 เปอร์เซ็นต์ยอมรับว่าบางครั้งปล่อยทิ้งไว้ สาเหตุหลักคือความรีบร้อน: “เดี๋ยวจะเพิ่มค่าคงที่ทีหลัง” — แต่ทีหลังนั้นไม่มีวันมา และหนึ่งเดือนต่อมา ตัวเลข 0.85 ก็ยังคงอยู่กลางเมธอดโดยไม่มีคำอธิบาย

กฎสำคัญ: ทุกค่าลิเทอรัลยกเว้น 0, 1, true, false และสตริงว่างควรถูกแยกเป็นค่าคงที่ที่มีชื่อ ข้อยกเว้น: การเพิ่มตัวนับ (i + 1), ศูนย์ทางคณิตศาสตร์ (การตรวจสอบ 0), และค่าเริ่มต้นของตัวสะสม ทุกอย่างอื่นเป็นตัวเลือกสำหรับการตั้งชื่อ

Magic numbers และเหตุใดจึงอันตราย

Magic number คือลิเทอรัลตัวเลขที่ค่าไม่ชัดเจนจากบริบท ตัวอย่างคลาสสิก: 86400 ในโค้ดที่เกี่ยวข้องกับหมดเวลา นักพัฒนามองเห็นตัวเลขและต้องเดาว่ามันคือจำนวนวินาทีในหนึ่งวัน ถ้าเขาทำผิดและเขียน 84600 บักจะจับได้ยากเพราะหมดเวลาจะทำงานเร็วกว่า 18 นาที

เหตุใด magic numbers จึงอันตราย: ประการแรก ทำให้อ่านยาก ตัวเลข 1024 อาจหมายถึงขนาดกิโลไบต์ เกณฑ์การแบ่งหน้า หรือจำนวนสูงสุดของรายการ หากไม่มีบริบท — มันก็แค่ตัวเลข ประการที่สอง สร้างความซ้ำซ้อน: ถ้า 1024 ใช้ในห้าตำแหน่ง เมื่อเกณฑ์เปลี่ยนเป็น 2048 นักพัฒนาต้องค้นหาทั้งห้าและแทนที่ หากพลาดตำแหน่งเดียว ระบบทำงานผิดพลาดแต่ไม่มีข้อผิดพลาดชัดเจน

ตัวอย่าง magic numbers ก่อนและหลัง

kotlin
// 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 หรือพาทใดควรปรากฏในโค้ดเป็นลิเทอรัลสตริง

swift
// 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 และพารามิเตอร์บูลีน

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

เครื่องมือตรวจจับมายากล

การค้นหาค่ามายากลเป็นระบบอัตโนมัติโดยตัววิเคราะห์แบบคงที่ที่ถูกกำหนดค่าให้ตรวจจับลิเทอรัลในตำแหน่งที่ไม่คาดคิด แต่ละภาษามีเครื่องมือของตัวเองพร้อมข้อยกเว้นที่ปรับแต่งได้

เครื่องมือภาษากฎ
SonarQubeJava, Kotlin, Swift, Python, JSMagicNumber, HardcodedString
ESLintJavaScript, TypeScriptno-magic-numbers, no-hardcoded-strings
DetektKotlinMagicNumber, ComplexCondition
SwiftLintSwiftmagic_number (opt-in)
PMDJava, Apex, PLSQLMagicNumber (รายการที่อนุญาตปรับแต่งได้)
การตรวจสอบ PhpStormPHPNumericLiteralWithContext (การตรวจสอบในตัว)

การกำหนดค่าข้อยกเว้นเป็นสิ่งสำคัญ — หากไม่มี ตัววิเคราะห์จะแจ้งเตือนทุกการเพิ่ม (-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) แต่ในเชิงความหมายแล้วเป็นปริมาณที่แตกต่างกันและไม่สามารถรวมเป็นค่าคงที่เดียวได้

java
// 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 เปอร์เซ็นต์?

ใช่ 100 ก็เป็น magic number ถ้าใช้โดยไม่มีบริบท แทนที่ 100 ให้เขียน MAX_PERCENT หรือ PROBABILITY_SCALE ข้อยกเว้น: เมื่อ 100 เป็นเปอร์เซ็นต์ที่ชัดเจนในบริบท (เช่น ในสูตรคำนวณเปอร์เซ็นต์) แต่แม้ในกรณีนี้ค่าคงที่ก็ช่วยให้อ่านง่ายขึ้น

แล้วตัวเลขในการทดสอบล่ะ?

ในการทดสอบก็ควรใช้ตัวแปรที่มีชื่อเช่นกัน แทนที่ assertEquals(42, result) ให้เขียน val expected = 42; assertEquals(expected, result) ข้อยกเว้น: การทดสอบค่าขอบเขต (0, null, สตริงว่าง) — สามารถปล่อยเป็นลิเทอรัลได้เพราะอ่านเข้าใจได้ในบริบทของการทดสอบ

ควรแยกตัวเลขไปยังทรัพยากร Android หรือไม่?

ใช่ ตัวเลขที่เกี่ยวข้องกับ UI (ขนาด ระยะขอบ ระยะเวลาแอนิเมชัน) ควรอยู่ในทรัพยากร (dimens.xml, integers.xml) ค่าคงที่ทางธุรกิจ (หมดเวลา ขีดจำกัด) — ใน companion object หรือไฟล์กำหนดค่า เกณฑ์หลัก: ถ้าตัวเลขสามารถเปลี่ยนได้โดยไม่เปลี่ยนตรรกะ — มันคือทรัพยากร

วิธีค้นหา magic numbers ในโปรเจกต์เก่า?

รัน SonarQube ด้วยกฎ MagicNumber หรือ ESLint ด้วย no-magic-numbers รับรายงาน เรียงตามความถี่ในการใช้งาน และเริ่มด้วยตัวเลขที่ปรากฏในสามตำแหน่งขึ้นไป มันเป็นตัวเลือกที่มีแนวโน้มมากที่สุดสำหรับการแยกเป็นค่าคงที่

ทุกตัวเลขในโค้ดควรถูกแยกเป็นค่าคงที่หรือไม่?

ไม่ ลิเทอรัลที่ยอมรับได้: 0, 1, -1 (เพิ่ม/ลด, ตรวจสอบว่าง), true, false, null, สตริงว่าง ที่เหลือทั้งหมดต้องการการตั้งชื่อ ถ้าตัวเลข 0 ไม่ได้ใช้เป็นการตรวจสอบว่าง (เช่น 0 คือ ID หมวดหมู่ราก) แล้ว 0 ก็ควรเป็นค่าคงที่: ROOT_CATEGORY_ID = 0

สรุป

  • มายากล — ลิเทอรัลที่ไม่มีคำอธิบาย: ตัวเลข สตริง แฟล็กที่ความหมายถูกซ่อนจากผู้อ่านโค้ด
  • Magic numbers — ค่าคงที่ตัวเลขที่ไม่มีชื่อ (86400, 1024, 0.85, 5000) ที่ต้องใช้ความรู้โดเมนเพื่อทำความเข้าใจ
  • สตริงมายากล — คีย์ URL และพาธที่ถูกฮาร์ดโค้ดซึ่งมองไม่เห็นโดยคอมไพเลอร์และนำไปสู่บักในเวลาทำงาน
  • Magic flags — พารามิเตอร์บูลีนที่ค่าไม่ชัดเจน (true/false ในการเรียกเมธอด)
  • เครื่องมือ: SonarQube, ESLint, Detekt, SwiftLint, PMD — ทั้งหมดสนับสนุนกฎ MagicNumber
  • วิธีแก้: แต่ละลิเทอรัล (ยกเว้น 0, ±1, true, false, null, "") ถูกแยกเป็นค่าคงที่ที่มีชื่อพร้อมชื่อที่อธิบายความหมาย
  • บริบทต่างกัน — ค่าคงที่ต่างกัน: 5000 เป็นหมดเวลาและ 5000 เป็นขนาดแคชเป็นเอนทิตีที่แตกต่างกัน

เราจะพัฒนาแอปพลิเคชันบนมือถือแบบครบวงจร

IT Sectr สร้างแอปพลิเคชัน iOS และ Android สำหรับสตาร์ทอัพและธุรกิจตั้งแต่ปี 2017 เราจะให้คำแนะนำและเสนอวิธีแก้ปัญหาที่ดีที่สุดแก่คุณ

ปรึกษาโครงการ

อ่านเพิ่มเติม