Магія в програмуванні — що це, небезпека магічних чисел та як їх замінити

Автор: IT Sectr Опубліковано: 2026-07-27 Час читання: 10 хв

Магія в програмуванні — це не метафора, а точний термін, що позначає значення (числа, рядки, прапорці), зміст яких неочевидний із контексту та потребує зовнішніх знань для розуміння. Найпоширеніший вид магії — магічні числа: числові константи, вписані прямо в код без пояснень, чому вибрано саме це значення. За даними SonarSource Code Quality Report (2025), близько 8 відсотків усіх попереджень статичних аналізаторів пов'язані з непоясненими літералами. Магічні значення роблять код крихким: зміна потребує пошуку всіх входжень, а новий розробник не розуміє, чи можна чіпати число, чи воно критичне для роботи системи.

Головне

  • Магія — неявні числа, рядки та прапорці в коді, зміст яких прихований від читача.
  • Магічні числа — числові літерали без назви: 86400, 3.14, 0.85, 1024.
  • Магічні рядки — жорстко закодовані шляхи, ключі, URL без винесення в константи.
  • Інструменти пошуку: SonarQube (правило MagicNumber), ESLint (no-magic-numbers), Detekt.
  • Рішення: виносити кожне магічне значення в іменовану константу з пояснювальною назвою.

Що таке магія в програмуванні?

Магія — це будь-яке значення у вихідному коді, чий зміст неочевидний без додаткових знань про предметну область. Термін усталений у спільноті: якщо розробник дивиться на число й не розуміє, звідки воно взялося, — це магія.

Магія буває кількох видів: числова (магічні числа), рядкова (магічні рядки), булева (магічні прапорці) та конфігураційна (жорстко закодовані параметри, які мають бути в налаштуваннях). Усі чотири види об'єднує одна проблема: при зміні вимоги розробник повинен знайти всі місця, де використовується значення, і замінити їх вручну. Пропуск хоча б одного входження призводить до багу.

За даними звіту JetBrains Code Quality Survey (2025), 73 відсотки розробників вважають магічні числа індикатором низької якості коду, при цьому 41 відсоток визнає, що самі періодично їх залишають. Основна причина — поспіх: «Я поставлю константу потім» — але потім не настає, і через місяць число 0.85 залишається без пояснення в тілі методу.

Ключове правило: будь-яке літеральне значення, окрім 0, 1, true, false та порожнього рядка, має бути винесене в іменовану константу. Винятки: інкремент лічильника (i + 1), математичні нулі (перевірка на 0) та початкові значення акумуляторів. Усе інше — кандидат на іменування.

Магічні числа та чому вони небезпечні

Магічне число — це числовий літерал, чиє значення не очевидне з контексту. Класичний приклад: 86400 у коді, що відповідає за таймаут. Розробник бачить число й має здогадатися, що це кількість секунд у добі. Якщо він помилиться й поставить 84600 — баг буде важко відловити, тому що таймаут спрацює на 18 хвилин раніше.

Чим небезпечні магічні числа: по-перше, вони порушують читабельність. Число 1024 може означати розмір кілобайта, поріг пагінації або максимальну кількість елементів. Без контексту — це просто число. По-друге, вони створюють дублювання: якщо 1024 використовується в п'яти місцях, при зміні порогу на 2048 розробник повинен знайти всі п'ять і замінити. Якщо одне місце пропущено — система працює некоректно, але без явної помилки.

Приклад магічних чисел до та після

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 — зупиніться й подумайте, чи варто винести його в константу. Якщо число пов'язане з бізнес-логікою (ліміт, поріг, таймаут, розмір) — виносьте обов'язково. Якщо число — математична константа (пі, e) — використовуйте стандартну бібліотеку (Math.PI, Math.E).

Магічні рядки та шляхи

Магічні рядки — рядкові літерали, вбудовані в код без винесення в константи або ресурси. Типові приклади: URL ендпоінтів, назви ключів SharedPreferences, Intent Actions, ключі bundle, імена файлів і SQL-запити.

Небезпека магічних рядків — у відсутності перевірки на етапі компіляції. Друк у рядку «user_prefs» не буде виявлено до рантайму. Якщо рядок використовується в десяти місцях, а розробник в одному написав «user_pref» (без s) — застосунок не падає, але дані не зберігаються. Такий баг може жити в продакшні місяцями, тому що не викликає crash.

Для Android-проектів магічні рядки мають бути винесені в ресурси (strings.xml, arrays.xml) або в константи в companion object. Для iOS — у рядкові ресурси (Localizable.strings) або константи enum. Для backend — у конфігураційні файли (.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 або константу гарантує, що всі посилання використовують одне й те саме значення.

Магічні прапорці та булеві параметри

Магічні прапорці — булеві параметри, значення яких не очевидне з контексту виклику. Класичний анти-патерн: передача true або false в метод без пояснення, що саме цей прапорець вмикає або вимикає.

Приклад: userDao.fetch(includeDeleted = false). Розробник бачить false і не розуміє, що це — «не вмикати видалені» чи «не вмикати активні». Через місяць false перетворюється на true, і у видачі починають з'являтися видалені записи. Баг виявляється тільки в продакшні.

Рішення — заміна булевих прапорців на enum або sealed class. Замість параметра Boolean використовуйте UserFilter.includeDeleted або UserFilter.activeOnly. Так код сам документує намір, і IDE підказує доступні варіанти при автодоповненні.

Якщо булевий прапорець передається через кілька шарів — це ще один сигнал, що абстракція неправильна. Замість того щоб тягти прапорець через три рівні викликів, подумайте, чи не повинен вибір фільтрації бути прийнятий на верхньому рівні й переданий як готова конфігурація. Чим менше булевих прапорців у коді — тим менше магії.

Запровадьте правило: жоден булевий параметр не передається в метод без іменованого аргументу (якщо мова підтримує named arguments). У 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 (можна налаштувати список дозволених)
PhpStorm InspectionsPHPNumericLiteralWithContext (вбудована інспекція)

Налаштування винятків критично важливе — без нього аналізатор буде видавати попередження на кожен інкремент (-1, +1) і математичний нуль. Для SonarQube список дозволених чисел: 0, 1, -1, 2 (для подвоєння), 100 (відсотки), 60 і 24 (час). Для всіх інших значень — вимагати іменовану константу з модифікатором public static final (Java) або const val (Kotlin).

Для аналізу на рівні CI додайте крок із перевіркою магії як warning, але не блокуючий збірку. Перший запуск покаже сотні попереджень у legacy-коді. Поступово, тікет за тікетом, переводьте код на константи й підвищуйте поріг якості. Коли кількість магічних чисел стане менше ніж 10 — вмикайте правило як помилку збірки.

Рефакторинг: замінюємо магію на константи

Рефакторинг магії — одна з найбезпечніших операцій: заміна літералу на константу не змінює поведінку коду. Тим не менш, підхід має бути системним, щоб не пропустити приховані залежності (наприклад, якщо одне магічне число використовується в непов'язаних контекстах, але випадково збігається за значенням).

Покроковий процес: знайти всі входження магічного значення, зрозуміти контекст кожного, рознести по різних константах (навіть якщо значення збігаються — контексти різні, і константи мають називатися по-різному), замінити літерали на константи, перевірити через тести. Помилка на кроці 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 магічним числом, якщо це 100 відсотків?

Так, 100 — теж магічне число, якщо використовується без контексту. Замість 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 або конфігураційному файлі. Головний критерій: якщо число може змінитися без зміни логіки — це ресурс.

Як знайти магічні числа в legacy-проєкті?

Запустіть SonarQube з правилом MagicNumber або ESLint із no-magic-numbers. Отримайте звіт, відсортуйте за частотою використання та почніть із чисел, які зустрічаються в трьох і більше місцях. Вони з найбільшою ймовірністю є кандидатами на виділення константи.

Чи будь-яке число в коді треба виносити в константу?

Ні. Допустимі літерали: 0, 1, -1 (інкремент/декремент, перевірка на порожнечу), true, false, null, порожній рядок. Усі інші — потребують іменування. Якщо число 0 використовується не як перевірка на порожнечу (наприклад, 0 — це ID кореневої категорії), то 0 теж має бути константою: ROOT_CATEGORY_ID = 0.

Підсумки

  • Магія — літерали без пояснення: числа, рядки, прапорці, чий зміст прихований від читача коду.
  • Магічні числа — числові константи без імені (86400, 1024, 0.85, 5000), що потребують доменних знань для розуміння.
  • Магічні рядки — жорстко закодовані ключі, URL і шляхи, невидимі для компілятора та що призводять до рантайм-багів.
  • Магічні прапорці — булеві параметри, значення яких не очевидне (true/false у виклику методу).
  • Інструменти: SonarQube, ESLint, Detekt, SwiftLint, PMD — усі підтримують правило MagicNumber.
  • Рішення: кожен літерал (крім 0, ±1, true, false, null, "") виноситься в іменовану константу з пояснювальною назвою.
  • Різні контексти — різні константи: 5000 як таймаут і 5000 як розмір кешу — це різні сутності.

Ми розробимо мобільний застосунок під ключ

IT Sectr створює застосунки для iOS та Android для стартапів і бізнесу з 2017 року. Ми проконсультуємо вас і запропонуємо найкраще рішення.

Обговорити проект

Читайте також