Magia w programowaniu — to nie metafora, a precyzyjny termin oznaczający wartości (liczby, łańcuchy, flagi), których znaczenie nie jest oczywiste z kontekstu i wymaga zewnętrznej wiedzy do zrozumienia. Najbardziej rozpowszechnionym rodzajem magii są magic numbers: stałe liczbowe wpisane bezpośrednio w kod bez wyjaśnienia, dlaczego wybrano właśnie tę wartość. Według badania SonarSource Code Quality Report (2025), około 8 procent wszystkich ostrzeżeń analizatorów statycznych związanych jest z niewyjaśnionymi literałami. Magiczne wartości czynią kod kruchym: zmiana wymaga znalezienia wszystkich wystąpień, a nowy programista nie rozumie, czy można dotknąć liczby, czy jest ona krytyczna dla działania systemu.
Główne
Magia (magic) — to każda wartość w kodzie źródłowym, której znaczenie nie jest oczywiste bez dodatkowej wiedzy o dziedzinie. Termin ten utrwalił się w społeczności: jeśli programista patrzy na liczbę i nie rozumie, skąd się wzięła — to magia.
Magia występuje w kilku rodzajach: liczbowa (magic numbers), tekstowa (magic strings), logiczna (magic flags) i konfiguracyjna (zakodowane na stałe parametry, które powinny być w ustawieniach). Wszystkie cztery rodzaje łączy jeden problem: przy zmianie wymagania programista musi znaleźć wszystkie miejsca, w których wartość jest używana, i zastąpić je ręcznie. Pominięcie choć jednego wystąpienia prowadzi do błędu.
Według raportu JetBrains Code Quality Survey (2025), 73 procent programistów uważa magic numbers za wskaźnik niskiej jakości kodu, przy czym 41 procent przyznaje, że sami od czasu do czasu je zostawiają. Główny powód — pośpiech: „postawię stałą później” — ale później nie nadchodzi, i po miesiącu liczba 0.85 pozostaje bez wyjaśnienia w ciele metody.
Kluczowa zasada: każda wartość literałowa, z wyjątkiem 0, 1, true, false i pustego łańcucha, powinna być wyodrębniona do nazwanej stałej. Wyjątki: inkrementacja licznika (i + 1), matematyczne zera (sprawdzenie na 0) i wartości początkowe akumulatorów. Cała reszta — kandydat do nazwania.
Magic number — to literał liczbowy, którego wartość nie jest oczywista z kontekstu. Klasyczny przykład: 86400 w kodzie odpowiedzialnym za timeout. Programista widzi liczbę i musi się domyślić, że to liczba sekund w dobie. Jeśli się pomyli i wstawi 84600 — błąd będzie trudny do wyłapania, ponieważ timeout zadziała 18 minut wcześniej.
Czym grożą magic numbers: po pierwsze, zakłócają czytelność. Liczba 1024 może oznaczać rozmiar kilobajta, próg paginacji lub maksymalną liczbę elementów. Bez kontekstu — to tylko liczba. Po drugie, tworzą powielanie: jeśli 1024 jest używane w pięciu miejscach, przy zmianie progu na 2048 programista musi znaleźć wszystkie pięć i zastąpić. Jeśli jedno miejsce zostanie pominięte — system działa nieprawidłowo, ale bez jawnego błędu.
// przed — magia w czystej postaci
fun calculateTimeout(base: Int): Int {
return base * 3 + 5000
}
// po — wartości zastąpione stałymi
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
}
Trzecie zagrożenie — niemożność testowania. Jeśli wartość progowa jest wbudowana w kod jako literał, test nie może jej nadpisać, aby sprawdzić warunki brzegowe. Stała wyodrębniona do companion object lub pliku konfiguracyjnego czyni kod testowalnym: test podstawia inną wartość i sprawdza zachowanie systemu na granicy.
Wyrob sobie nawyk: za każdym razem, gdy piszesz liczbę inną niż 0, 1, 100 lub 2 — zatrzymaj się i zastanów, czy warto wyodrębnić ją do stałej. Jeśli liczba jest związana z logiką biznesową (limit, próg, timeout, rozmiar) — wyodrębniaj obowiązkowo. Jeśli liczba jest stałą matematyczną (pi, e) — używaj standardowej biblioteki (Math.PI, Math.E).
Magic strings — literały tekstowe wbudowane w kod bez wyodrębnienia do stałych lub zasobów. Typowe przykłady: URL endpointów, nazwy kluczy SharedPreferences, Intent Actions, bundle keys, nazwy plików i zapytania SQL.
Zagrożenie magicznymi łańcuchami polega na braku sprawdzania na etapie kompilacji. Literówka w łańcuchu „user_prefs” nie zostanie wykryta aż do runtime'u. Jeśli łańcuch jest używany w dziesięciu miejscach, a programista w jednym napisał „user_pref” (bez s) — aplikacja nie pada, ale dane nie są zapisywane. Taki błąd może żyć w produkcji miesiącami, ponieważ nie powoduje crasha.
Dla projektów Android magiczne łańcuchy powinny być wyodrębnione do zasobów (strings.xml, arrays.xml) lub stałych w companion object. Dla iOS — do zasobów tekstowych (Localizable.strings) lub stałych enum. Dla backendu — do plików konfiguracyjnych (.env, application.properties). Żaden klucz, URL ani ścieżka nie powinny występować w kodzie jako literał tekstowy.
// przed — magiczne łańcuchy w całej klasie
let prefs = UserDefaults.standard
prefs.set(token, forKey: "auth_token")
prefs.set(userId, forKey: "current_user_id")
// po — łańcuchy wyodrębnione do 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)
Szczególną uwagę zwracaj na łańcuchy, które się powielają. Jeśli ten sam klucz „user_settings” występuje w trzech plikach — z prawdopodobieństwem 99 procent prędzej czy później w jednym z nich pojawi się literówka. Wyodrębnienie do enum lub stałej gwarantuje, że wszystkie odniesienia używają tej samej wartości.
Magic flags — parametry logiczne, których wartość nie jest oczywista z kontekstu wywołania. Klasyczny anty-wzorzec: przekazanie true lub false do metody bez wyjaśnienia, co dokładnie ta flaga włącza lub wyłącza.
Przykład: userDao.fetch(includeDeleted = false). Programista widzi false i nie rozumie, czy to — „nie uwzględniaj usuniętych” czy „nie uwzględniaj aktywnych”. Za miesiąc false zamienia się w true, a w wynikach zaczynają pojawiać się usunięte rekordy. Błąd jest wykrywany dopiero na produkcji.
Rozwiązanie — zastąpienie flag logicznych enumem lub sealed class. Zamiast parametru Boolean używaj UserFilter.includeDeleted lub UserFilter.activeOnly. W ten sposób kod sam dokumentuje intencję, a IDE podpowiada dostępne opcje przy autouzupełnianiu.
Jeśli flaga logiczna jest przekazywana przez kilka warstw — to kolejny sygnał, że abstrakcja jest nieprawidłowa. Zamiast przeciągać flagę przez trzy poziomy wywołań, zastanów się, czy wybór filtrowania nie powinien być podjęty na najwyższym poziomie i przekazany jako gotowa konfiguracja. Im mniej flag logicznych w kodzie — tym mniej magii.
Wprowadź zasadę: żaden parametr logiczny nie jest przekazywany do metody bez nazwanego argumentu (jeśli język obsługuje named arguments). W Kotlin i Swift to wymaganie jest spełniane automatycznie. W Java używaj Buildera lub stałych enum zamiast true/false.
Wyszukiwanie magicznych wartości jest automatyzowane przez analizatory statyczne, które są skonfigurowane do wykrywania literałów w nieoczekiwanych miejscach. Każdy język oferuje własne narzędzia z konfigurowalnymi wyjątkami.
| Narzędzie | Języki | Reguła |
|---|---|---|
| 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 (włączony opt-in) |
| PMD | Java, Apex, PLSQL | MagicNumber (można skonfigurować listę dozwolonych) |
| PhpStorm Inspections | PHP | NumericLiteralWithContext (wbudowana inspekcja) |
Konfiguracja wyjątków jest krytycznie ważna — bez niej analizator będzie wyświetlał ostrzeżenia przy każdej inkrementacji (-1, +1) i matematycznym zerze. Dla SonarQube lista dozwolonych liczb: 0, 1, -1, 2 (dla podwojenia), 100 (procenty), 60 i 24 (czas). Dla wszystkich pozostałych wartości — wymagaj nazwanej stałej z modyfikatorem public static final (Java) lub const val (Kotlin).
Do analizy na poziomie CI dodaj krok ze sprawdzaniem magii jako warning, ale nie blokujący budowania. Pierwsze uruchomienie pokaże setki ostrzeżeń w legacy-kodzie. Stopniowo, ticket za ticketem, przenoś kod na stałe i podnoś próg jakości. Gdy liczba magic numbers spadnie poniżej 10 — włącz regułę jako błąd budowania.
Refaktoring magii — jedna z najbezpieczniejszych operacji: zastąpienie literału stałą nie zmienia zachowania kodu. Niemniej jednak podejście powinno być systematyczne, aby nie przeoczyć ukrytych zależności (na przykład, jeśli jedno magic number jest używane w niepowiązanych kontekstach, ale przypadkowo ma tę samą wartość).
Proces krok po kroku: znajdź wszystkie wystąpienia magicznej wartości, zrozum kontekst każdego, rozdziel na różne stałe (nawet jeśli wartości są takie same — konteksty są różne, a stałe powinny nazywać się inaczej), zastąp literały stałymi, sprawdź przez testy. Błąd na kroku 2 — najczęstszy: dwa różne pojęcia (timeout w milisekundach i próg w bajtach) mogą liczbowo być takie same (na przykład 5000), ale semantycznie to różne wielkości i nie można ich łączyć w jedną stałą.
// przed — ta sama liczba w różnych kontekstach
public class Config {
public void setupCache() {
cache.setMaxSize(5000); // 5 MB
}
public void setupTimeout() {
client.setReadTimeout(5000); // 5 sekund
}
}
// po — różne stałe dla różnych kontekstów
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
);
}
}
Dla nowego kodu zasada jest prosta: każdy literał, z wyjątkiem 0, 1, -1, true, false, null i pustego łańcucha, jest wyodrębniany do stałej. Wyjątki: stałe matematyczne (zawsze przez standardową bibliotekę), dane testowe (można zostawić literał w teście, ale z opisującą nazwą zmiennej) i wartości brzegowe dla inkrementacji (i + 1 w pętli — normalne).
Często zadawane pytania
Tak, 100 też jest magic number, jeśli jest używane bez kontekstu. Zamiast 100 napisz MAX_PERCENT lub PROBABILITY_SCALE. Wyjątek: gdy 100 to oczywisty procent w kontekście (na przykład we wzorze obliczania procentu), ale nawet w tym przypadku stała poprawia czytelność.
W testach też lepiej używać nazwanych zmiennych. Zamiast assertEquals(42, result) napisz val expected = 42; assertEquals(expected, result). Wyjątek: testy na wartości brzegowe (0, null, pusty łańcuch) — można je zostawić jako literały, ponieważ są czytelne w kontekście testu.
Tak, liczby związane z UI (rozmiary, marginesy, czas trwania animacji) powinny być w zasobach (dimens.xml, integers.xml). Stałe biznesowe (timeouty, limity) — w companion object lub pliku konfiguracyjnym. Głównym kryterium jest: jeśli liczba może się zmienić bez zmiany logiki — to zasób.
Uruchom SonarQube z regułą MagicNumber lub ESLint z no-magic-numbers. Otrzymaj raport, posortuj według częstotliwości użycia i zacznij od liczb, które występują w trzech lub więcej miejscach. One z największym prawdopodobieństwem są kandydatami do wyodrębnienia stałej.
Nie. Dozwolone literały: 0, 1, -1 (inkrementacja/dekrementacja, sprawdzanie na pustość), true, false, null, pusty łańcuch. Wszystkie pozostałe wymagają nazwania. Jeśli liczba 0 jest używana nie jako sprawdzenie na pustość (na przykład 0 — to ID kategorii głównej), to 0 też powinno być stałą: ROOT_CATEGORY_ID = 0.
Podsumowanie
Opracujemy aplikację mobilną pod klucz
IT Sectr tworzy aplikacje na iOS i Androida dla startupów i firm od 2017 roku. Doradzimy Ci i zaproponujemy najlepsze rozwiązanie.
Przeczytaj również