Магия в программировании — это не метафора, а точный термин, обозначающий значения (числа, строки, флаги), смысл которых неочевиден из контекста и требует внешних знаний для понимания. Самый распространённый вид магии — magic numbers: числовые константы, вписанные прямо в код без пояснений, почему выбрано именно это значение. По данным исследования SonarSource Code Quality Report (2025), около 8 процентов всех предупреждений статических анализаторов связаны с необъяснёнными литералами. Магические значения делают код хрупким: изменение требует поиска всех вхождений, а новый разработчик не понимает, можно ли трогать число или оно критично для работы системы.
Главное
Магия (magic) — это любое значение в исходном коде, чей смысл неочевиден без дополнительных знаний о предметной области. Термин устоялся в сообществе: если разработчик смотрит на число и не понимает, откуда оно взялось, — это магия.
Магия бывает нескольких видов: числовая (magic numbers), строковая (magic strings), булева (magic flags) и конфигурационная (захардкоженные параметры, которые должны быть в settings). Все четыре вида объединяет одна проблема: при изменении requirement разработчик должен найти все места, где значение используется, и заменить их вручную. Пропуск хотя бы одного вхождения приводит к багу.
По данным отчёта JetBrains Code Quality Survey (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 — остановитесь и подумайте, стоит ли вынести его в константу. Если число связано с бизнес-логикой (лимит, порог, таймаут, размер) — выносите обязательно. Если число — математическая константа (пи, e) — используйте стандартную библиотеку (Math.PI, Math.E).
Magic strings — строковые литералы, встроенные в код без вынесения в константы или ресурсы. Типичные примеры: URL эндпоинтов, названия SharedPreferences-ключей, Intent Actions, bundle keys, имена файлов и sql-запросы.
Опасность магических строк — в отсутствии проверки на этапе компиляции. Опечатка в строке «user_prefs» не будет обнаружена до рантайма. Если строка используется в десяти местах, а разработчик в одном написал «user_pref» (без s), — приложение не падает, но данные не сохраняются. Такой баг может жить в продакшне месяцами, потому что не вызывает crash.
Для Android-проектов магические строки должны быть вынесены в resources (strings.xml, arrays.xml) или в константы в companion object. Для iOS — в строковые ресурсы (Localizable.strings) или константы enum. Для backend — в конфигурационные файлы (.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 class. Вместо параметра Boolean используйте UserFilter.includeDeleted или UserFilter.activeOnly. Так код сам документирует намерение, и IDE подсказывает доступные варианты при автодополнении.
Если булев флаг передаётся через несколько слоёв — это ещё один сигнал, что абстракция неправильная. Вместо того чтобы тащить флаг через три уровня вызовов, подумайте, не должен ли выбор фильтрации быть принят на верхнем уровне и передан как готовая конфигурация. Чем меньше булевых флагов в коде — тем меньше магии.
Внедрите правило: ни один булев параметр не передаётся в метод без именованного аргумента (если язык поддерживает named arguments). В 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 Inspections | PHP | NumericLiteralWithContext (встроенная инспекция) |
Настройка исключений критически важна без неё анализатор будет выдавать предупреждения на каждый инкремент (-1, +1) и математический ноль. Для SonarQube список разрешённых чисел: 0, 1, -1, 2 (для удвоения), 100 (проценты), 60 и 24 (время). Для всех остальных значений — требовать именованную константу с модификатором public static final (Java) или const val (Kotlin).
Для анализа на уровне CI добавьте шаг с проверкой магии как warning, но не блокирующий сборку. Первый запуск покажет сотни предупреждений в legacy-коде. Постепенно, тикет за тикетом, переводите код на константы и повышайте порог качества. Когда количество 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 c no-magic-numbers. Получите отчёт, отсортируйте по частоте использования и начинайте с чисел, которые встречаются в трёх и более местах. Они с наибольшей вероятностью являются кандидатами на выделение константы.
Нет. Допустимые литералы: 0, 1, -1 (инкремент/декремент, проверка на пустоту), true, false, null, пустая строка. Все остальные — требуют именования. Если число 0 используется не как проверка на пустоту (например, 0 — это ID корневой категории), то 0 тоже должен быть константой: ROOT_CATEGORY_ID = 0.
Итоги
Мы разработаем мобильное приложение под ключ
IT Sectr создаёт приложения для iOS и Android для стартапов и бизнеса с 2017 года. Мы проконсультируем вас и предложим наилучшее решение.
Читайте также