Магията в програмирането — не е метафора, а точен термин, обозначаващ стойности (числа, низове, флагове), чийто смисъл не е очевиден от контекста и изисква външни знания за разбиране. Най-разпространеният вид магия — magic numbers: числови константи, вписани директно в кода без обяснение защо е избрана точно тази стойност. Според изследване на SonarSource Code Quality Report (2025), около 8 процента от всички предупреждения на статичните анализатори са свързани с необяснени литерали. Магически стойности правят кода крехък: промяната изисква търсене на всички срещания, а новият разработчик не разбира дали може да пипа числото или то е критично за работата на системата.
Основни
Магия (magic) — е всяка стойност в изходния код, чийто смисъл не е очевиден без допълнителни знания за предметната област. Терминът се е утвърдил в общността: ако разработчикът погледне число и не разбира откъде идва — това е магия.
Магията бива няколко вида: числова (magic numbers), низова (magic strings), булева (magic flags) и конфигурационна (хардкоднати параметри, които трябва да са в настройките). И четирите вида ги обединява един проблем: при промяна на изискване разработчикът трябва да намери всички места, където се използва стойността, и да ги замени ръчно. Пропускането дори на едно срещане води до грешка.
Според доклада JetBrains Code Quality Survey (2025), 73 процента от разработчиците смятат magic numbers за индикатор за ниско качество на кода, докато 41 процента признават, че самите те понякога ги оставят. Основната причина — бързина: „Ще сложа константата по-късно“ — но по-късно не идва и след месец числото 0.85 остава без обяснение в тялото на метода.
Ключовото правило: всяка литерална стойност, освен 0, 1, true, false и празния низ, трябва да бъде изнесена в именувана константа. Изключения: инкрементиране на брояч (i + 1), математически нули (проверка за 0) и начални стойности на акумулатори. Всичко останало — кандидат за именуване.
Magic number — числов литерал, чиято стойност не е очевидна от контекста. Класически пример: 86400 в кода, отговорен за timeout. Разработчикът вижда числото и трябва да познае, че това е броят секунди в денонощие. Ако сгреши и сложи 84600 — грешката ще бъде трудна за улавяне, защото timeout-ът ще се активира 18 минути по-рано.
Защо magic numbers са опасни: първо, те нарушават четимостта. Числото 1024 може да означава размер на килобайт, праг на пагинация или максимален брой елементи. Без контекст — това е просто число. Второ, те създават дублиране: ако 1024 се използва на пет места, при промяна на прага на 2048 разработчикът трябва да намери всичките пет и да ги замени. Ако едно място е пропуснато — системата работи неправилно, но без явна грешка.
// преди — магия в чист вид
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
}
Третата опасност — невъзможност за тестване. Ако праговата стойност е вградена в кода като литерал, тестът не може да я презапише, за да провери граничните условия. Константа, изнесена в companion object или конфигурационен файл, прави кода тестваем: тестът задава друга стойност и проверява поведението на системата на границата.
Изградете навик: всеки път, когато пишете число, различно от 0, 1, 100 или 2 — спрете се и помислете дали си струва да го изнесете в константа. Ако числото е свързано с бизнес логика (лимит, праг, timeout, размер) — изнасяйте го задължително. Ако числото е математическа константа (pi, e) — използвайте стандартната библиотека (Math.PI, Math.E).
Magic strings — низови литерали, вградени в кода без изнасяне в константи или ресурси. Типични примери: URL на крайни точки, имена на 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 или път не трябва да присъства в кода като низов литерал.
// преди — магически низове в целия клас
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 — булеви параметри, чиято стойност не е очевидна от контекста на извикването. Класически анти-модел: предаване на 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 ниво добавете стъпка с проверка на магията като предупреждение, но не блокиращо изграждането. Първото пускане ще покаже стотици предупреждения в наследен код. Постепенно, тикет по тикет, прехвърляйте кода на константи и повишавайте прага на качество. Когато броят на magic numbers стане по-малък от 10 — включете правилото като грешка при изграждане.
Рефакторингът на магия — една от най-безопасните операции: замяната на литерал с константа не променя поведението на кода. Въпреки това подходът трябва да бъде систематичен, за да не се пропуснат скрити зависимости (например, ако едно и също magic number се използва в несвързани контексти, но случайно съвпада по стойност).
Процес стъпка по стъпка: намерете всички срещания на магическата стойност, разберете контекста на всяко, разпределете в различни константи (дори ако стойностите съвпадат — контекстите са различни и константите трябва да се наричат различно), заменете литералите с константи, проверете чрез тестове. Грешката в стъпка 2 — най-честата: две различни понятия (timeout в милисекунди и праг в байтове) могат числено да съвпадат (например 5000), но семантично това са различни величини и не могат да бъдат обединени в една константа.
// преди — едно и също число в различни контексти
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
);
}
}
За нов код правилото е просто: всеки литерал, освен 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). Бизнес константи (timeout-и, лимити) — в companion object или конфигурационен файл. Основният критерий: ако числото може да се промени без промяна на логиката — то е ресурс.
Стартирайте SonarQube с правило MagicNumber или ESLint с no-magic-numbers. Получете отчет, сортирайте по честота на използване и започнете с числата, които се срещат на три или повече места. Те с най-голяма вероятност са кандидати за изнасяне в константа.
Не. Разрешени литерали: 0, 1, -1 (инкрементиране/декрементиране, проверка за празнота), true, false, null, празен низ. Всички останали изискват именуване. Ако числото 0 се използва не като проверка за празнота (например 0 — е ID на коренна категория), то тогава 0 също трябва да бъде константа: ROOT_CATEGORY_ID = 0.
Обобщение
Ще разработим мобилно приложение под ключ
IT Sectr създава iOS и Android приложения за стартъпи и бизнеси от 2017 г. Ще ви консултираме и ще предложим най-доброто решение.
Прочетете също