Code Smell to powierzchowny symptom w kodzie, który sygnalizuje potencjalny problem w projekcie lub architekturze aplikacji. Termin wprowadził Kent Beck, a spopularyzował Martin Fowler w książce „Refactoring: Improving the Design of Existing Code“. Według Martina Fowlera, zapach kodu niekoniecznie oznacza błąd, ale prawie zawsze wskazuje na potrzebę refaktoryzacji w celu poprawy utrzymywalności.
Najważniejsze
Code Smell (zapach kodu) — to metafora oznaczająca symptomy w kodzie źródłowym, które z dużym prawdopodobieństwem wskazują na głębsze problemy. Sam termin nie ma formalnej definicji — to heurystyka oparta na doświadczeniu programistów. Martin Fowler i Kent Beck w 1999 roku po raz pierwszy usystematyzowali 22 zapachy w książce „Refactoring“, a większość z nich pozostaje aktualna po dziesięcioleciach.
Ważne jest zrozumienie różnicy między Code Smell a błędem. Zapach to nie błąd: kod się kompiluje, działa i zwraca poprawny wynik. Problem polega na tym, że taki kod jest trudny do czytania, modyfikowania i testowania. Z czasem koszt każdej zmiany rośnie, a pewność co do poprawności refaktoryzacji maleje. Narzędzia analizy statycznej (SonarQube, Detekt, SwiftLint) automatycznie wykrywają wiele zapachów.
Heurystyczny charakter Code Smell oznacza, że nie każdą długą metodę trzeba dzielić i nie każda duża klasa wymaga refaktoryzacji. Decyzję podejmuje programista, oceniając kontekst: częstotliwość zmian, krytyczność modułu, plany rozwoju. Doświadczeni inżynierowie wyczuwają zapach intuicyjnie — kod „nieprzyjemnie pachnie“, mimo że formalnie wszystkie zasady są przestrzegane.
Fowler wyróżnił 22 zapachy, które dzielą się na kilka kategorii. Dla programowania mobilnego najbardziej istotne są zapachy strukturalne, zapachy projektowania obiektowego oraz specyficzne problemy związane z ograniczeniami platformy. Przyjrzyjmy się każdej grupie na przykładach z rzeczywistej praktyki.
Long Method (długa metoda) — najczęstszy zapach w aplikacjach mobilnych. Ekran z formularzem rejestracji często zawiera jedną metodę setupUI o długości 200+ linii, która tworzy wszystkie widoki, konfiguruje ograniczenia, subskrybuje zdarzenia i obsługuje błędy. Rozwiązanie: podzielić na metody według bloków logicznych — configureEmailField, configurePasswordField, setupConstraints, bindViewModel.
Large Class (duża klasa) — Activity lub ViewController, który odpowiada i za wyświetlanie, i za nawigację, i za logikę biznesową, i za komunikację sieciową. Taka klasa narusza Single Responsibility Principle i zawiera dziesiątki pól i metod. W Androidzie często jest to Fragment z 1000+ linii zawierający logikę różnych ekranów. Rozwiązanie: wydzielić presenter/ViewModel, przenieść pracę z siecią do repozytorium, nawigację do koordynatora.
Duplicate Code (duplikacja kodu) — kopiowanie tych samych bloków w różnych częściach aplikacji. Typowy przykład: dwa ekrany wyświetlające kartę produktu — w katalogu i w ulubionych. Jeśli logika wyświetlania jest skopiowana, poprawa błędu w jednym miejscu nie naprawi go w drugim. Rozwiązanie: przenieść wspólną logikę do komponentu wielokrotnego użytku lub rozszerzenia.
Feature Envy (zazdrość o cudzą klasę) — metoda jednej klasy intensywnie korzysta z danych innej klasy. W Androidzie objawia się to, gdy ViewModel bezpośrednio odwołuje się do pól modelu User zamiast wywołać metodę modelu. Sygnał: jeśli metodę można przenieść do klasy, której dane wykorzystuje — przenieś ją. Switch Statements (łańcuchy warunków) — konstrukcja switch lub ciąg if-else sprawdzający typ obiektu. Zamiast tego należy użyć polimorfizmu lub wzorca strategy.
Data Class — klasa, która tylko przechowuje dane, ale nie zawiera zachowania. Same w sobie data class (w Kotlin) lub struktury (w Swift) nie są zapachem. Problem pojawia się, gdy logika biznesowa działająca na tych danych jest rozrzucona po całej bazie kodu zamiast być hermetyzowana. Refused Bequest — dziedziczący nie używa większości metod rodzica i nadpisuje je pustymi makietami. Oznaka nieprawidłowego dziedziczenia: zastąp dziedziczenie kompozycją.
God Activity / God Fragment — Activity lub Fragment, które wiedzą o wszystkim: o cyklu życia, o danych, o nawigacji, o uprawnieniach, o DI. To najdroższa w utrzymaniu klasa aplikacji. Rozwiązanie: wzorce architektoniczne MVVM, MVI lub Clean Architecture dzielą odpowiedzialność. Giant ViewController — analog dla iOS, gdzie UIViewController zawiera całą logikę ekranu i często przekracza 500 linii.
Hardcoded Resources — ciągi znaków, kolory, rozmiary, adresy URL API wbudowane bezpośrednio w kod. W Androidzie narusza to użycie systemu zasobów R, w iOS — NSLocalizedString i Asset Catalog. Poprawka: przenieść wszystkie ciągi do strings.xml lub Localizable.strings, adresy URL do pliku konfiguracyjnego, rozmiary do dimens. Leaking Context — przechowywanie referencji do Activity lub ViewController dłużej niż żyje sam komponent. Prowadzi do wycieków pamięci i awarii. Rozwiązanie: słabe referencje, Jetpack Lifecycle, RxSwift DisposeBag.
| Zapach | Gdzie występuje | Rozwiązanie |
|---|---|---|
| Long Method | Android/iOS | Extract Method, podział |
| Large Class | Activity, ViewController | MVVM, VIPER, Clean Arch |
| Duplicate Code | Dowolne ekrany | Shared Component, DRY |
| Feature Envy | ViewModel, Presenter | Move Method |
| Leaking Context | Android | Komponenty świadome cyklu życia |
Code review — najskuteczniejszy sposób wykrywania zapachów. Ludzkie oko zauważa nienaturalne konstrukcje, które automatyczne analizatory pomijają. Skuteczność przeglądu kodu wzrasta, gdy zespół korzysta z listy kontrolnej typowych zapachów. Zaleca się sprawdzanie nie więcej niż 200–400 linii kodu w jednej sesji — po tym progu uwaga spada, a zapachy zaczynają umykać.
Analiza statyczna automatyzuje wyszukiwanie zapachów strukturalnych. Dla Androida standardowym narzędziem jest Detekt (Kotlin) i Android Lint, dla iOS — SwiftLint i SonarQube. Te narzędzia znajdują długie metody, duże klasy, duplikację kodu i wiele innych problemów. Ważne jest skonfigurowanie reguł pod projekt — domyślne konfiguracje często są zbyt rygorystyczne lub przeciwnie, pomijają krytyczne zapachy.
Metryki kodu dają obiektywne kryteria: Cyclomatic Complexity (próg >10 wymaga uwagi), Lines of Code per Method (próg >30), Depth of Inheritance (>3 — powód do zastanowienia). Narzędzia takie jak CodeMetrics (Xcode) i Gradle Metrics Plugin budują wykresy zmian metryk w czasie. Jeśli złożoność metody wzrosła z 5 do 15 po ostatnim commicie — to sygnał do refaktoryzacji.
// Przykład: metoda o złożoności cyklomatycznej = 7 (powyżej progu 5)
fun processOrder(order: Order) {
if (order.status == Status.NEW) { /* 10 wierszy */ }
else if (order.status == Status.PAID) { /* 15 wierszy */ }
else if (order.status == Status.SHIPPED) { /* 20 wierszy */ }
else if (order.status == Status.DELIVERED) { /* 8 wierszy */ }
else if (order.status == Status.CANCELLED) { /* 5 wierszy */ }
else { throw IllegalStateException() }
}
// Poprawka: polimorfizm zamiast switch
interface OrderHandler {
fun handle(order: Order)
}
Automatyczne wyszukiwanie zapachów nie zastępuje przeglądu kodu: analizatory statyczne znajdują tylko problemy strukturalne, ale nie wychwytują zapachów semantycznych (Feature Envy, Inappropriate Intimacy). Połączenie narzędzi automatycznych i ludzkiej kontroli daje najlepszy rezultat. Skonfiguruj potok CI/CD tak, aby kompilacja padała przy przekroczeniu progów złożoności lub długości metody.
Refaktoryzacja — główna metoda eliminacji zapachów kodu. Fowler opisuje dziesiątki technik refaktoryzacji, z których każda ma zastosowanie do konkretnego zapachu. Extract Method — dla długich metod, Extract Class — dla dużych klas, Move Method — dla Feature Envy. Ważne jest wykonywanie refaktoryzacji małymi krokami, zachowując działanie kodu po każdej zmianie.
Testy przed refaktoryzacją — obowiązkowy warunek. Jeśli kod nie jest pokryty testami jednostkowymi, refaktoryzacja zamienia się w przepisywanie z nieznanym wynikiem. Dla kodu legacy, gdzie nie ma testów, użyj Characterisation Tests — napisz testy, które utrwalają bieżące zachowanie, a następnie refaktoryzuj. Testowanie daje pewność, że po refaktoryzacji logika biznesowa nie jest uszkodzona.
Stopniowość — klucz do skutecznego usuwania zapachów w programowaniu mobilnym. Nie próbuj przepisywać God Activity w całości. Wydziel najpierw warstwę nawigacji, potem warstwę danych, następnie logikę wyświetlania. Każdy krok opatrz commitem i przebiegiem testów. Używaj feature toggle, aby włączyć refaktoryzację dla części użytkowników i wycofać w razie problemów.
Narzędzia IDE automatyzują wiele technik refaktoryzacji. Android Studio i IntelliJ IDEA oferują wbudowane refaktoryzacje: Extract Method, Extract Interface, Pull Members Up, Encapsulate Fields. Xcode (począwszy od wersji 14) poprawił obsługę refaktoryzacji dla Swifta. Korzystanie z automatycznych refaktoryzacji zmniejsza ryzyko błędów w porównaniu z ręcznym kopiowaniem kodu.
Programowanie mobilne dodaje własne specyficzne zapachy związane z ograniczeniami platformy. W Androidzie są to wyciek Context, niezamknięte Cursor, nieprawidłowe użycie Lifecycle. W iOS — retain cycle przez domknięcia, nieprawidłowa praca z Auto Layout, gigantyczne ViewController. Te zapachy nie tylko pogarszają utrzymywalność, ale także bezpośrednio wpływają na wydajność i stabilność aplikacji.
Callback Hell — charakterystyczny zapach dla kodu pracującego z operacjami asynchronicznymi. Zagnieżdżone callbacki (callback inside callback) sprawiają, że kod staje się nieczytelny i trudny do debugowania. Rozwiązanie: korutyny (Kotlin), async/await (Swift 5.5+), RxJava/RxSwift lub Combine. Według Google I/O 2023, projekty, które przeszły ze stylu callbackowego na korutyny, zmniejszają liczbę błędów o 30% i przyspieszają dodawanie nowych funkcji.
Platform Coupling — sztywne powiązanie logiki biznesowej z komponentami platformy. Testowanie takiej logiki wymaga uruchomienia emulatora, co spowalnia pętlę sprzężonia zwrotnego. Poprawka: Clean Architecture dzieli kod na warstwy Domain (czysty Kotlin/Swift bez zależności od platformy) i Data/UI (z zależnościami platformowymi). Logika biznesowa testowana jest na JVM bez emulatora.
Często zadawane pytania
Nie — Code Smell nie jest błędem. Kod z zapachem działa poprawnie, ale jest trudny w utrzymaniu, modyfikacji i testowaniu. Błąd to nieprawidłowe działanie, zapach to ostrzeżenie o potencjalnych problemach w przyszłości.
22 zapachy w drugim wydaniu książki „Refactoring“ (2019). Wśród nich Long Method, Large Class, Primitive Obsession, Data Clumps, Switch Statements, Speculative Generality i inne. Społeczność dodała dziesiątki nowych zapachów dla współczesnych paradygmatów i platform.
Połączenie daje najlepszy wynik: Detekt (Android/Kotlin), SwiftLint (iOS), SonarQube (oba) do automatycznej analizy i code review do zapachów semantycznych. Żadne narzędzie nie znajduje 100% problemów — ludzkie doświadczenie pozostaje decydujące.
Można, jeśli kod zmienia się rzadko lub zostanie całkowicie przepisany w najbliższym czasie. Jednak nagromadzenie zapachów zamienia się w dług techniczny: każda nowa zmiana jest coraz trudniejsza, a koszt naprawy rośnie wykładniczo.
Tak — frameworki deklaratywne stworzyły nowe zapachy: gigantyczne bloki @State, nieprawidłowa praca z powtórnymi renderowaniami, nadmierna recomposition, brak ekstrakcji do osobnych widoków. Dla SwiftUI typowym zapachem jest Massive View z dziesiątkami zmiennych @State.
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ż