جادو در برنامه‌نویسی — چیست، چرا magic numbers خطرناک هستند و جایگزینی آن

نویسنده: IT Sectr منتشر شده: 2026-07-27 زمان مطالعه: 10 دقیقه

جادو در برنامه‌نویسی — یک استعاره نیست، بلکه یک اصطلاح دقیق است که به مقادیری (اعداد، رشته‌ها، پرچم‌ها) اشاره دارد که معنای آن‌ها از زمینه آشکار نیست و برای درک به دانش خارجی نیاز دارد. رایج‌ترین نوع جادو — magic numbers: ثابت‌های عددی که مستقیماً در کد بدون توضیح نوشته شده‌اند که چرا این مقدار خاص انتخاب شده است. بر اساس گزارش SonarSource Code Quality Report (2025)، حدود 8 درصد از همه هشدارهای تحلیلگرهای ایستا با literals توضیح‌داده‌نشده مرتبط هستند. مقادیر جادویی کد را شکننده می‌کنند: تغییر نیاز به جستجوی همه موارد دارد و توسعه‌دهنده جدید نمی‌فهمد که آیا می‌توان عدد را تغییر داد یا برای عملکرد سیستم حیاتی است.

نکات اصلی

  • جادو — اعداد، رشته‌ها و پرچم‌های پنهان در کد که معنای آن‌ها از خواننده پنهان است.
  • Magic numbers — literals عددی بدون نام: 86400, 3.14, 0.85, 1024.
  • رشته‌های جادویی — hardcode مسیرها، کلیدها، URL بدون استخراج به ثابت‌ها.
  • ابزارهای جستجو: SonarQube (قانون MagicNumber)، ESLint (no-magic-numbers)، Detekt.
  • راه‌حل: استخراج هر مقدار جادویی به یک ثابت نام‌گذاری شده با نام توضیحی.

جادو در برنامه‌نویسی چیست؟

جادو (magic) — هر مقدار در کد منبع است که معنای آن بدون دانش اضافی درباره حوزه موضوعی آشکار نیست. این اصطلاح در جامعه جا افتاده است: اگر توسعه‌دهنده به عددی نگاه می‌کند و نمی‌فهمد از کجا آمده — این جادو است.

جادو چند نوع دارد: عددی (magic numbers)، رشته‌ای (magic strings)، بولی (magic flags) و پیکربندی (پارامترهای hardcoded که باید در تنظیمات باشند). هر چهار نوع یک مشکل مشترک دارند: هنگام تغییر نیازمندی، توسعه‌دهنده باید تمام مکان‌هایی که مقدار استفاده شده را پیدا کند و به‌صورت دستی جایگزین کند. از دست دادن حتی یک مورد منجر به باگ می‌شود.

بر اساس گزارش JetBrains Code Quality Survey (2025)، 73 درصد توسعه‌دهندگان magic numbers را نشانگر کیفیت پایین کد می‌دانند، در حالی که 41 درصد اعتراف می‌کنند که خودشان هم گاهی آن‌ها را باقی می‌گذارند. دلیل اصلی — عجله: «بعداً ثابت را می‌گذارم» — اما بعداً هرگز نمی‌رسد و پس از یک ماه عدد 0.85 بدون توضیح در بدنه متد باقی می‌ماند.

قانون کلیدی: هر literal به جز 0, 1, true, false و رشته خالی باید به یک ثابت نام‌گذاری شده استخراج شود. استثناها: افزایش شمارنده (i + 1)، صفرهای ریاضی (بررسی 0) و مقادیر اولیه accumulatorها. بقیه موارد — نامزد نام‌گذاری هستند.

Magic numbers و چرا خطرناک هستند

Magic number — literal عددی است که مقدار آن از زمینه آشکار نیست. مثال کلاسیک: 86400 در کدی که مسئول timeout است. توسعه‌دهنده عدد را می‌بیند و باید حدس بزند که این تعداد ثانیه در یک روز است. اگر اشتباه کند و 84600 بگذارد — باگ به سختی پیدا می‌شود، زیرا timeout 18 دقیقه زودتر فعال می‌شود.

چرا magic numbers خطرناک هستند: اولاً، آن‌ها خوانایی را مختل می‌کنند. عدد 1024 می‌تواند به معنی اندازه کیلوبایت، آستانه صفحه‌بندی یا حداکثر تعداد عناصر باشد. بدون زمینه — این فقط یک عدد است. ثانیاً، آن‌ها تکرار ایجاد می‌کنند: اگر 1024 در پنج مکان استفاده شود، هنگام تغییر آستانه به 2048 توسعه‌دهنده باید هر پنج مورد را پیدا کرده و جایگزین کند. اگر یک مکان از قلم بیفتد — سیستم نادرست کار می‌کند، اما بدون خطای آشکار.

مثال magic numbers قبل و بعد

kotlin
// قبل — جادو به شکل خالص
fun calculateTimeout(base: Int): Int {
    return base * 3 + 5000
}

// بعد — مقادیر با ثابت‌ها جایگزین شدند
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
}

خطر سوم — عدم امکان آزمایش. اگر مقدار آستانه در کد به صورت literal hardcoded شده باشد، تست نمی‌تواند آن را برای بررسی شرایط مرزی بازنویسی کند. ثابت استخراج شده به companion object یا فایل پیکربندی، کد را قابل آزمایش می‌کند: تست مقدار دیگری قرار می‌دهد و رفتار سیستم را در مرز بررسی می‌کند.

عادت ایجاد کنید: هر بار که عددی غیر از 0, 1, 100 یا 2 می‌نویسید — توقف کنید و فکر کنید که آیا ارزش استخراج به ثابت را دارد. اگر عدد با منطق کسب‌وکار مرتبط است (محدودیت، آستانه، timeout، اندازه) — حتماً استخراج کنید. اگر عدد یک ثابت ریاضی است (pi, e) — از کتابخانه استاندارد استفاده کنید (Math.PI, Math.E).

رشته‌ها و مسیرهای جادویی

Magic strings — literalهای رشته‌ای که بدون استخراج به ثابت‌ها یا منابع در کد تعبیه شده‌اند. مثال‌های معمول: URLهای endpoint، نام کلیدهای SharedPreferences، Intent Actions، bundle keys، نام فایل‌ها و کوئری‌های SQL.

خطر رشته‌های جادویی در عدم بررسی در مرحله کامپایل است. اشتباه تایپی در رشته «user_prefs» تا زمان runtime شناسایی نخواهد شد. اگر رشته در ده مکان استفاده شود و توسعه‌دهنده در یکی «user_pref» (بدون s) نوشته باشد — برنامه کرش نمی‌کند، اما داده‌ها ذخیره نمی‌شوند. چنین باگی می‌تواند ماه‌ها در تولید زنده بماند، زیرا باعث کرش نمی‌شود.

برای پروژه‌های Android، رشته‌های جادویی باید به منابع (strings.xml, arrays.xml) یا ثابت‌های companion object استخراج شوند. برای iOS — به منابع رشته‌ای (Localizable.strings) یا ثابت‌های enum. برای backend — به فایل‌های پیکربندی (.env, application.properties). هیچ کلید، URL یا مسیری نباید در کد به صورت literal رشته‌ای وجود داشته باشد.

swift
// قبل — رشته‌های جادویی در سراسر کلاس
let prefs = UserDefaults.standard
prefs.set(token, forKey: "auth_token")
prefs.set(userId, forKey: "current_user_id")

// بعد — رشته‌ها به 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 class. به جای پارامتر Boolean از UserFilter.includeDeleted یا UserFilter.activeOnly استفاده کنید. به این ترتیب کد خودش قصد را مستند می‌کند و IDE گزینه‌های موجود را در تکمیل خودکار پیشنهاد می‌دهد.

اگر پرچم بولی از چند لایه عبور می‌کند — این سیگنال دیگری است که انتزاع اشتباه است. به جای کشیدن پرچم از سه سطح فراخوانی، فکر کنید که آیا انتخاب فیلتر باید در سطح بالایی گرفته شده و به عنوان پیکربندی آماده ارسال شود. هرچه پرچم‌های بولی در کد کمتر باشد — جادو کمتر است.

قاعده‌ای معرفی کنید: هیچ پارامتر بولی بدون آرگومان نام‌گذاری شده به متد ارسال نمی‌شود (اگر زبان از named arguments پشتیبانی می‌کند). در Kotlin و Swift این نیاز به‌طور خودکار برآورده می‌شود. در Java به جای true/false از Builder یا ثابت‌های enum استفاده کنید.

ابزارهای تشخیص جادو

جستجوی مقادیر جادویی توسط تحلیلگرهای ایستا که برای شناسایی literalها در مکان‌های غیرمنتظره پیکربندی شده‌اند، خودکار می‌شود. هر زبان ابزارهای خود را با استثناهای قابل تنظیم ارائه می‌دهد.

ابزارزبان‌هاقانون
SonarQubeJava, Kotlin, Swift, Python, JSMagicNumber, HardcodedString
ESLintJavaScript, TypeScriptno-magic-numbers, no-hardcoded-strings
DetektKotlinMagicNumber, ComplexCondition
SwiftLintSwiftmagic_number (فعال به صورت opt-in)
PMDJava, Apex, PLSQLMagicNumber (می‌توان لیست مجازها را تنظیم کرد)
PhpStorm InspectionsPHPNumericLiteralWithContext (بازرسی داخلی)

پیکربندی استثناها حیاتی است — بدون آن تحلیلگر در هر افزایش (-1, +1) و صفر ریاضی هشدار می‌دهد. برای SonarQube لیست اعداد مجاز: 0, 1, -1, 2 (برای دوبرابر کردن), 100 (درصدها), 60 و 24 (زمان). برای بقیه مقادیر — ثابت نام‌گذاری شده با اصلاح‌کننده public static final (Java) یا const val (Kotlin) الزامی است.

برای تحلیل در سطح CI یک مرحله بررسی جادو به عنوان warning اضافه کنید، اما مسدودکننده build نباشد. اولین اجرا صدها هشدار در کد قدیمی نشان می‌دهد. به تدریج، ticket به ticket، کد را به ثابت‌ها منتقل کنید و آستانه کیفیت را بالا ببرید. وقتی تعداد magic numbers کمتر از 10 شد — قانون را به عنوان خطای build فعال کنید.

بازآرایی: جادو را با ثابت‌ها جایگزین می‌کنیم

بازآرایی جادو — یکی از امن‌ترین عملیات‌ها: جایگزینی literal با ثابت رفتار کد را تغییر نمی‌دهد. با این وجود، رویکرد باید سیستماتیک باشد تا وابستگی‌های پنهان از دست نروند (مثلاً اگر یک magic number در زمینه‌های نامرتبط استفاده شود، اما تصادفاً مقدار یکسان باشد).

فرآیند گام به گام: تمام موارد مقدار جادویی را پیدا کنید، زمینه هر کدام را بفهمید، به ثابت‌های مختلف تقسیم کنید (حتی اگر مقادیر یکسان باشند — زمینه‌ها متفاوت هستند و ثابت‌ها باید متفاوت نام‌گذاری شوند)، literalها را با ثابت‌ها جایگزین کنید، از طریق تست‌ها بررسی کنید. خطا در مرحله 2 — رایج‌ترین: دو مفهوم متفاوت (timeout به میلی‌ثانیه و آستانه به بایت) ممکن است عدداً یکسان باشند (مثلاً 5000)، اما از نظر معنایی این کمیت‌های متفاوتی هستند و نمی‌توان آن‌ها را در یک ثابت ترکیب کرد.

java
// قبل — عدد یکسان در زمینه‌های مختلف
public class Config {
    public void setupCache() {
        cache.setMaxSize(5000); // 5 MB
    }
    public void setupTimeout() {
        client.setReadTimeout(5000); // 5 ثانیه
    }
}

// بعد — ثابت‌های مختلف برای زمینه‌های مختلف
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
        );
    }
}

برای کد جدید قانون ساده است: هر literal به جز 0, 1, -1, true, false, null و رشته خالی به ثابت استخراج می‌شود. استثناها: ثابت‌های ریاضی (همیشه از طریق کتابخانه استاندارد), داده‌های تست (می‌توان literal را در تست باقی گذاشت، اما با نام متغیر توضیحی) و مقادیر مرزی برای افزایش (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, رشته خالی) — می‌توان آن‌ها را به صورت literal باقی گذاشت، زیرا در زمینه تست خوانا هستند.

آیا ارزش استخراج اعداد به منابع Android را دارد؟

بله، اعداد مرتبط با UI (اندازه‌ها, فاصله‌ها, مدت انیمیشن) باید در منابع باشند (dimens.xml, integers.xml). ثابت‌های تجاری (timeoutها, محدودیت‌ها) — در companion object یا فایل پیکربندی. معیار اصلی: اگر عدد می‌تواند بدون تغییر منطق تغییر کند — این یک منبع است.

چگونه magic numbers را در پروژه قدیمی پیدا کنیم؟

SonarQube را با قانون MagicNumber یا ESLint را با no-magic-numbers اجرا کنید. گزارش دریافت کنید، بر اساس فراوانی استفاده مرتب کنید و از اعدادی که در سه یا بیشتر مکان یافت می‌شوند شروع کنید. آن‌ها با بیشترین احتمال کاندیدای استخراج به ثابت هستند.

آیا هر عددی در کد باید به ثابت استخراج شود؟

خیر. literals مجاز: 0, 1, -1 (افزایش/کاهش, بررسی خالی بودن), true, false, null, رشته خالی. بقیه نیاز به نام‌گذاری دارند. اگر عدد 0 نه به عنوان بررسی خالی بودن استفاده می‌شود (مثلاً 0 — ID دسته ریشه), پس 0 هم باید ثابت باشد: ROOT_CATEGORY_ID = 0.

خلاصه

  • جادو — literals بدون توضیح: اعداد, رشته‌ها, پرچم‌هایی که معنای آن‌ها از خواننده کد پنهان است.
  • Magic numbers — ثابت‌های عددی بدون نام (86400, 1024, 0.85, 5000) که برای درک به دانش دامنه نیاز دارند.
  • Magic strings — hardcode کلیدها, URL و مسیرها, نامرئی برای کامپایلر و منجر به باگ‌های runtime.
  • Magic flags — پارامترهای بولی که مقدار آن‌ها آشکار نیست (true/false در فراخوانی متد).
  • ابزارها: SonarQube, ESLint, Detekt, SwiftLint, PMD — همه از قانون MagicNumber پشتیبانی می‌کنند.
  • راه‌حل: هر literal (به جز 0, ±1, true, false, null, «») به یک ثابت نام‌گذاری شده با نام توضیحی استخراج می‌شود.
  • زمینه‌های مختلف — ثابت‌های مختلف: 5000 به عنوان timeout و 5000 به عنوان اندازه کش — موجودیت‌های متفاوتی هستند.

ما یک اپلیکیشن موبایل به صورت کلید در دست توسعه خواهیم داد

IT Sectr از سال 2017 برنامه‌های iOS و Android را برای استارتاپ‌ها و کسب‌وکارها ایجاد می‌کند. ما به شما مشاوره می‌دهیم و بهترین راه‌حل را پیشنهاد خواهیم کرد.

بحث درباره پروژه

همچنین بخوانید