Ma thuật trong lập trình — nó là gì, sự nguy hiểm của magic numbers và cách thay thế

Tác giả: IT Sectr Đã đăng: 2026-07-27 Thời gian đọc: 10 phút

Ma thuật trong lập trình không phải là một phép ẩn dụ, mà là một thuật ngữ chính xác chỉ các giá trị (số, chuỗi, cờ) mà ý nghĩa của chúng không rõ ràng từ ngữ cảnh và đòi hỏi kiến thức bên ngoài để hiểu. Loại ma thuật phổ biến nhất là magic numbers: các hằng số số được viết trực tiếp vào mã mà không giải thích tại sao giá trị cụ thể đó được chọn. Theo Báo cáo Chất lượng Mã của SonarSource (2025), khoảng 8 phần trăm tất cả các cảnh báo của trình phân tích tĩnh liên quan đến các literal không được giải thích. Các giá trị ma thuật làm cho mã trở nên dễ vỡ: thay đổi chúng đòi hỏi phải tìm tất cả các vị trí xuất hiện và một nhà phát triển mới không biết liệu có thể sửa số hay nó quan trọng cho hoạt động của hệ thống.

Những điểm chính

  • Ma thuật — các số, chuỗi và cờ ngầm trong mã mà ý nghĩa bị ẩn khỏi người đọc.
  • Magic numbers — các literal số không có tên: 86400, 3.14, 0.85, 1024.
  • Chuỗi ma thuật — đường dẫn, khóa, URL được hardcode mà không đưa vào hằng số.
  • Công cụ phát hiện: SonarQube (quy tắc MagicNumber), ESLint (no-magic-numbers), Detekt.
  • Giải pháp: đưa mỗi giá trị ma thuật vào một hằng số có tên với tên mô tả.

Ma thuật trong lập trình là gì?

Ma thuật là bất kỳ giá trị nào trong mã nguồn mà ý nghĩa của nó không rõ ràng nếu không có kiến thức miền bổ sung. Thuật ngữ này đã được thiết lập trong cộng đồng: nếu một nhà phát triển nhìn vào một số và không thể nói nó đến từ đâu — đó là ma thuật.

Ma thuật có nhiều loại: số (magic numbers), chuỗi (magic strings), boolean (magic flags) và cấu hình (các tham số được hardcode lẽ ra phải ở trong cài đặt). Cả bốn loại đều chia sẻ một vấn đề: khi một yêu cầu thay đổi, nhà phát triển phải tìm mọi nơi giá trị được sử dụng và thay thế chúng bằng tay. Bỏ sót một vị trí sẽ dẫn đến lỗi.

Theo Khảo sát Chất lượng Mã của JetBrains (2025), 73 phần trăm nhà phát triển coi magic numbers là chỉ báo của chất lượng mã thấp, trong khi 41 phần trăm thừa nhận rằng họ thỉnh thoảng để lại chúng. Lý do chính là vội vàng: “Tôi sẽ thêm hằng số sau” — nhưng “sau” không bao giờ đến, và một tháng sau, số 0.85 vẫn nằm giữa thân phương thức mà không có giải thích.

Quy tắc chính: mọi giá trị literal ngoại trừ 0, 1, true, false và chuỗi rỗng phải được đưa vào hằng số có tên. Ngoại lệ: tăng bộ đếm (i + 1), số không toán học (kiểm tra 0) và giá trị ban đầu của bộ tích lũy. Mọi thứ khác đều là ứng cử viên để đặt tên.

Magic numbers và tại sao chúng nguy hiểm

Magic number là một literal số mà giá trị của nó không rõ ràng từ ngữ cảnh. Một ví dụ kinh điển: 86400 trong mã liên quan đến thời gian chờ. Nhà phát triển nhìn thấy số và phải đoán rằng đó là số giây trong một ngày. Nếu anh ta mắc lỗi và viết 84600, lỗi sẽ khó bị phát hiện vì thời gian chờ sẽ kích hoạt sớm hơn 18 phút.

Tại sao magic numbers nguy hiểm: thứ nhất, chúng làm giảm khả năng đọc. Số 1024 có thể có nghĩa là kích thước kilobyte, ngưỡng phân trang hoặc số lượng mục tối đa. Không có ngữ cảnh — nó chỉ là một con số. Thứ hai, chúng tạo ra sự trùng lặp: nếu 1024 được sử dụng ở năm nơi, khi ngưỡng thay đổi thành 2048, nhà phát triển phải tìm cả năm và thay thế chúng. Nếu bỏ sót một nơi, hệ thống hoạt động không chính xác nhưng không có lỗi rõ ràng.

Ví dụ về magic numbers trước và sau

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
}

Mối nguy hiểm thứ ba là không thể kiểm thử. Nếu giá trị ngưỡng được hardcode dưới dạng literal, bài kiểm tra không thể ghi đè nó để xác minh các điều kiện biên. Một hằng số được đưa vào companion object hoặc tệp cấu hình làm cho mã có thể kiểm thử được: bài kiểm tra thay thế một giá trị khác và xác minh hành vi của hệ thống tại biên.

Hãy phát triển một thói quen: mỗi khi bạn viết một số không phải 0, 1, 100 hoặc 2 — hãy dừng lại và cân nhắc xem có nên đưa nó vào hằng số không. Nếu số liên quan đến logic kinh doanh (giới hạn, ngưỡng, thời gian chờ, kích thước) — hãy đưa ra mà không do dự. Nếu số là hằng số toán học (pi, e) — hãy sử dụng thư viện chuẩn (Math.PI, Math.E).

Chuỗi và đường dẫn ma thuật

Chuỗi ma thuật là các literal chuỗi được nhúng vào mã mà không được đưa vào hằng số hoặc tài nguyên. Ví dụ điển hình: URL endpoint, tên khóa SharedPreferences, Intent Action, khóa bundle, tên tệp và truy vấn SQL.

Sự nguy hiểm của chuỗi ma thuật là thiếu kiểm tra tại thời điểm biên dịch. Một lỗi chính tả trong chuỗi “user_prefs” sẽ không bị phát hiện cho đến thời gian chạy. Nếu chuỗi được sử dụng ở mười nơi và nhà phát triển viết “user_pref” (thiếu s) ở một trong số chúng — ứng dụng không bị treo, nhưng dữ liệu không được lưu. Lỗi như vậy có thể tồn tại trong sản phẩm hàng tháng vì nó không gây ra sự cố.

Đối với các dự án Android, chuỗi ma thuật nên được đưa vào tài nguyên (strings.xml, arrays.xml) hoặc hằng số trong companion object. Đối với iOS — vào tài nguyên chuỗi (Localizable.strings) hoặc hằng số enum. Đối với backend — vào tệp cấu hình (.env, application.properties). Không có khóa, URL hoặc đường dẫn nào nên xuất hiện trong mã dưới dạng literal chuỗi.

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)

Hãy đặc biệt chú ý đến các chuỗi bị trùng lặp. Nếu cùng một khóa “user_settings” xuất hiện trong ba tệp — 99 phần trăm khả năng cuối cùng sẽ có lỗi chính tả ở một trong số chúng. Việc đưa vào enum hoặc hằng số đảm bảo rằng tất cả các tham chiếu đều sử dụng cùng một giá trị.

Magic flags và tham số boolean

Magic flags là các tham số boolean mà ý nghĩa của chúng không rõ ràng từ ngữ cảnh gọi. Một phản mẫu kinh điển: truyền true hoặc false cho một phương thức mà không giải thích chính xác cờ đó bật hoặc tắt điều gì.

Ví dụ: userDao.fetch(includeDeleted = false). Nhà phát triển thấy false và không thể biết nó có nghĩa là “không bao gồm đã xóa” hay “không bao gồm đang hoạt động”. Một tháng sau, false biến thành true và các bản ghi đã xóa bắt đầu xuất hiện trong đầu ra. Lỗi chỉ được phát hiện trong sản xuất.

Giải pháp là thay thế cờ boolean bằng enum hoặc lớp sealed. Thay vì tham số Boolean, hãy sử dụng UserFilter.includeDeleted hoặc UserFilter.activeOnly. Bằng cách này, mã tự ghi lại ý định của nó và IDE gợi ý các tùy chọn có sẵn trong quá trình tự động hoàn thành.

Nếu một cờ boolean được truyền qua nhiều lớp — đó là một dấu hiệu khác cho thấy sự trừu tượng sai. Thay vì kéo một cờ qua ba cấp độ gọi, hãy cân nhắc xem việc chọn bộ lọc có nên được thực hiện ở cấp cao nhất và được truyền dưới dạng cấu hình sẵn sàng hay không. Càng ít cờ boolean trong mã — càng ít ma thuật.

Hãy áp dụng một quy tắc: không có tham số boolean nào được truyền vào phương thức mà không có đối số có tên (nếu ngôn ngữ hỗ trợ đối số có tên). Trong Kotlin và Swift, yêu cầu này được tự động đáp ứng. Trong Java, hãy sử dụng Builder hoặc hằng số enum thay vì true/false.

Công cụ phát hiện ma thuật

Việc tìm kiếm các giá trị ma thuật được tự động hóa bởi các trình phân tích tĩnh được cấu hình để phát hiện các literal ở những nơi không mong đợi. Mỗi ngôn ngữ cung cấp các công cụ riêng với các ngoại lệ có thể tùy chỉnh.

Công cụNgôn ngữQuy tắc
SonarQubeJava, Kotlin, Swift, Python, JSMagicNumber, HardcodedString
ESLintJavaScript, TypeScriptno-magic-numbers, no-hardcoded-strings
DetektKotlinMagicNumber, ComplexCondition
SwiftLintSwiftmagic_number (opt-in)
PMDJava, Apex, PLSQLMagicNumber (danh sách cho phép có thể cấu hình)
Kiểm tra PhpStormPHPNumericLiteralWithContext (kiểm tra tích hợp)

Việc cấu hình ngoại lệ rất quan trọng — nếu không có nó, trình phân tích sẽ cảnh báo trên mọi số tăng (-1, +1) và số không toán học. Đối với SonarQube, danh sách số được phép: 0, 1, -1, 2 (để nhân đôi), 100 (phần trăm), 60 và 24 (thời gian). Đối với tất cả các giá trị khác — yêu cầu hằng số có tên với bổ từ public static final (Java) hoặc const val (Kotlin).

Đối với phân tích ở cấp CI, hãy thêm một bước kiểm tra ma thuật như cảnh báo nhưng không chặn bản dựng. Lần chạy đầu tiên sẽ hiển thị hàng trăm cảnh báo trong mã kế thừa. Dần dần, từng ticket một, di chuyển mã sang hằng số và nâng ngưỡng chất lượng. Khi số lượng magic numbers giảm xuống dưới 10 — hãy bật quy tắc dưới dạng lỗi bản dựng.

Tái cấu trúc: thay thế ma thuật bằng hằng số

Tái cấu trúc ma thuật là một trong những thao tác an toàn nhất: thay thế literal bằng hằng số không thay đổi hành vi của mã. Tuy nhiên, cách tiếp cận phải có hệ thống để không bỏ sót các phụ thuộc ẩn (ví dụ: nếu cùng một magic number được sử dụng trong các ngữ cảnh không liên quan nhưng tình cờ có cùng giá trị).

Quy trình từng bước: tìm tất cả các vị trí xuất hiện của giá trị ma thuật, hiểu ngữ cảnh của từng cái, phân chia thành các hằng số khác nhau (ngay cả khi các giá trị trùng khớp — ngữ cảnh khác nhau và hằng số nên được đặt tên khác nhau), thay thế các literal bằng hằng số, xác minh thông qua kiểm thử. Sai lầm ở bước 2 là phổ biến nhất: hai khái niệm khác nhau (thời gian chờ tính bằng mili giây và ngưỡng tính bằng byte) có thể trùng khớp về mặt số (ví dụ: 5000), nhưng về mặt ngữ nghĩa, chúng là các đại lượng khác nhau và không thể kết hợp thành một hằng số.

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
        );
    }
}

Đối với mã mới, quy tắc rất đơn giản: bất kỳ literal nào ngoại trừ 0, 1, -1, true, false, null và chuỗi rỗng đều được đưa vào hằng số. Ngoại lệ: hằng số toán học (luôn sử dụng thư viện chuẩn), dữ liệu kiểm thử (literal có thể giữ lại trong kiểm thử nhưng với tên biến mô tả) và giá trị biên cho số tăng (i + 1 trong vòng lặp là ổn).

Câu hỏi thường gặp

100 có phải là magic number nếu nó có nghĩa là 100 phần trăm?

Có, 100 cũng là magic number nếu được sử dụng mà không có ngữ cảnh. Thay vì 100, hãy viết MAX_PERCENT hoặc PROBABILITY_SCALE. Ngoại lệ: khi 100 rõ ràng là phần trăm trong ngữ cảnh (ví dụ: trong công thức tính phần trăm), nhưng ngay cả trong trường hợp này, hằng số cũng cải thiện khả năng đọc.

Còn các số trong bài kiểm tra thì sao?

Trong kiểm thử, tốt hơn nên sử dụng các biến có tên. Thay vì assertEquals(42, result), hãy viết val expected = 42; assertEquals(expected, result). Ngoại lệ: kiểm thử giá trị biên (0, null, chuỗi rỗng) — chúng có thể giữ lại dưới dạng literal vì chúng có thể đọc được trong ngữ cảnh kiểm thử.

Có nên đưa các số vào tài nguyên Android không?

Có, các số liên quan đến giao diện người dùng (kích thước, lề, thời lượng hoạt ảnh) nên ở trong tài nguyên (dimens.xml, integers.xml). Hằng số kinh doanh (thời gian chờ, giới hạn) — trong companion object hoặc tệp cấu hình. Tiêu chí chính: nếu một số có thể thay đổi mà không thay đổi logic — thì đó là tài nguyên.

Làm thế nào để tìm magic numbers trong dự án kế thừa?

Chạy SonarQube với quy tắc MagicNumber hoặc ESLint với no-magic-numbers. Lấy báo cáo, sắp xếp theo tần suất sử dụng và bắt đầu với các số xuất hiện ở ba nơi trở lên. Chúng là những ứng cử viên có khả năng nhất để đưa vào hằng số.

Có phải mọi số trong mã đều nên được đưa vào hằng số?

Không. Các literal chấp nhận được: 0, 1, -1 (tăng/giảm, kiểm tra rỗng), true, false, null, chuỗi rỗng. Tất cả các số khác đều yêu cầu đặt tên. Nếu số 0 không được sử dụng để kiểm tra rỗng (ví dụ: 0 là ID danh mục gốc), thì 0 cũng phải là hằng số: ROOT_CATEGORY_ID = 0.

Tổng kết

  • Ma thuật — literal không giải thích: số, chuỗi, cờ mà ý nghĩa bị ẩn khỏi người đọc mã.
  • Magic numbers — hằng số số không tên (86400, 1024, 0.85, 5000) đòi hỏi kiến thức miền để hiểu.
  • Chuỗi ma thuật — khóa, URL và đường dẫn hardcode vô hình với trình biên dịch và dẫn đến lỗi thời gian chạy.
  • Magic flags — tham số boolean mà giá trị không rõ ràng (true/false trong lời gọi phương thức).
  • Công cụ: SonarQube, ESLint, Detekt, SwiftLint, PMD — tất cả đều hỗ trợ quy tắc MagicNumber.
  • Giải pháp: mỗi literal (ngoại trừ 0, ±1, true, false, null, "") được đưa vào hằng số có tên với tên mô tả.
  • Ngữ cảnh khác nhau — hằng số khác nhau: 5000 là thời gian chờ và 5000 là kích thước bộ nhớ đệm là các thực thể khác nhau.

Chúng tôi sẽ phát triển ứng dụng di động chìa khóa trao tay

IT Sectr tạo các ứng dụng iOS và Android cho các công ty khởi nghiệp và doanh nghiệp từ năm 2017. Chúng tôi sẽ tư vấn và đề xuất giải pháp tốt nhất cho bạn.

Thảo luận dự án

Đọc thêm