Code Smell — це поверхнева ознака в коді, яка сигналізує про потенційну проблему в дизайні або архітектурі застосунку. Термін ввів Кент Бек і популяризував Мартін Фаулер у книзі «Refactoring: Improving the Design of Existing Code». За даними Martin Fowler, запах коду не обов'язково означає баг, але майже завжди вказує на необхідність рефакторингу для поліпшення підтримуваності.
Головне
Code Smell (запах коду) — це метафора для позначення симптомів у вихідному коді, які з високою ймовірністю вказують на глибші проблеми. Сам термін не має формального визначення — це евристика, заснована на досвіді розробників. Мартін Фаулер і Кент Бек у 1999 році вперше систематизували 22 запахи в книзі «Refactoring», і більшість із них залишаються актуальними десятиліття потому.
Важливо розуміти різницю між Code Smell і багом. Запах — це не помилка: код компілюється, працює і видає коректний результат. Проблема в тому, що такий код важко читати, змінювати та тестувати. З часом вартість кожної зміни зростає, а впевненість у коректності рефакторингу падає. Інструменти статичного аналізу (SonarQube, Detekt, SwiftLint) виявляють багато запахів автоматично.
Евристичний характер Code Smell означає, що не кожен довгий метод потрібно розбивати, і не кожен великий клас потребує рефакторингу. Рішення приймає розробник, оцінюючи контекст: частота змін, критичність модуля, плани на розвиток. Досвідчені інженери бачать запах інтуїтивно — код «неприємно пахне», хоча формально всі правила дотримані.
Фаулер виділив 22 запахи, які діляться на кілька категорій. Для мобільної розробки найбільш актуальні структурні запахи, запахи об'єктно-орієнтованого дизайну та специфічні проблеми, пов'язані з платформеними обмеженнями. Розглянемо кожну групу на прикладах із реальної практики.
Long Method (довгий метод) — найпоширеніший запах у мобільних застосунках. Екран з формою реєстрації часто містить один метод setupUI довжиною 200+ рядків, який створює всі View, налаштовує констрейнти, підписується на події та обробляє помилки. Рішення: розбити на методи за логічними блоками — configureEmailField, configurePasswordField, setupConstraints, bindViewModel.
Large Class (великий клас) — Activity або ViewController, який відповідає і за відображення, і за навігацію, і за бізнес-логіку, і за мережеву взаємодію. Такий клас порушує принцип єдиної відповідальності та містить десятки полів і методів. В Android це часто Fragment з 1000+ рядків, що містить логіку різних екранів. Рішення: виділити презентер/ViewModel, винести роботу з мережею в репозиторій, навігацію — в координатор.
Duplicate Code (дублювання коду) — копіювання однакових блоків у різних частинах застосунку. Типовий приклад: два екрани, що відображають картку товару, — в каталозі та в обраному. Якщо логіка відображення скопійована, виправлення багу в одному місці не виправить його в іншому. Рішення: винести спільну логіку в перевикористовуваний компонент або екстеншн.
Feature Envy (заздрість до чужого класу) — метод одного класу інтенсивно використовує дані іншого класу. В Android це проявляється, коли ViewModel напряму звертається до полів моделі User замість того, щоб викликати метод моделі. Сигнал: якщо метод можна перенести в клас, дані якого він використовує, — перенесіть. Switch Statements (ланцюжки умов) — конструкція switch або ланцюжок if-else, що перевіряє тип об'єкта. Замість цього слід використовувати поліморфізм або strategy pattern.
Data Class — клас, який тільки зберігає дані, але не містить поведінки. Самі по собі data class (в Kotlin) або структури (в Swift) не є запахом. Проблема виникає, коли бізнес-логіка, що працює з цими даними, розмазана по всій кодовій базі замість того, щоб бути інкапсульованою. Refused Bequest — спадкоємець не використовує більшу частину методів батька і перевизначає їх порожніми заглушками. Ознака неправильного успадкування: замініть успадкування на композицію.
God Activity / God Fragment — Activity або Fragment, які знають про все: про життєвий цикл, про дані, про навігацію, про Permissions, про DI. Це найдорогий у підтримці клас застосунку. Рішення: архітектурні патерни MVVM, MVI або Clean Architecture розділяють відповідальність. Giant ViewController — аналог для iOS, де UIViewController містить всю логіку екрана і часто перевищує 500 рядків.
Hardcoded Resources — рядки, кольори, розміри, API URL, вбудовані безпосередньо в код. В Android це порушує використання ресурсної системи R, в iOS — NSLocalizedString і Asset Catalog. Виправлення: виносити всі рядки в strings.xml або Localizable.strings, URL в конфігураційний файл, розміри в dimens. Leaking Context — зберігання посилання на Activity або ViewController довше, ніж живе сам компонент. Призводить до витоків пам'яті та кражів. Рішення: слабкі посилання, Jetpack Lifecycle, RxSwift DisposeBag.
| Запах | Де зустрічається | Рішення |
|---|---|---|
| Long Method | Android/iOS | Extract Method, розбиття |
| Large Class | Activity, ViewController | MVVM, VIPER, Clean Arch |
| Duplicate Code | Будь-які екрани | Shared Component, DRY |
| Feature Envy | ViewModel, Presenter | Move Method |
| Leaking Context | Android | Lifecycle-aware компоненти |
Code review — найнадійніший спосіб виявлення запахів. Людське око помічає неприродні конструкції, які автоматичні аналізатори пропускають. Ефективність код-рев'ю підвищується, якщо команда використовує чек-лист із типових запахів. Рекомендується перевіряти не більше 200–400 рядків коду за одну сесію — після цього порогу увага падає, і запахи починають вислизати.
Статичний аналіз автоматизує пошук структурних запахів. Для Android стандартний інструмент — Detekt (Kotlin) і Android Lint, для iOS — SwiftLint і SonarQube. Ці інструменти знаходять довгі методи, великі класи, дублювання коду та безліч інших проблем. Важливо налаштувати правила під проєкт — дефолтні конфігурації часто надто суворі або, навпаки, пропускають критичні запахи.
Метрики коду дають об'єктивні критерії: Cyclomatic Complexity (поріг >10 потребує уваги), Lines of Code per Method (поріг >30), Depth of Inheritance (>3 — привід задуматися). Інструменти на кшталт CodeMetrics (Xcode) і Gradle Metrics Plugin будують графіки зміни метрик у часі. Якщо складність методу зросла з 5 до 15 після останнього коміту — це сигнал до рефакторингу.
// Приклад: метод зі складністю Cyclomatic = 7 (вище порогу 5)
fun processOrder(order: Order) {
if (order.status == Status.NEW) { /* 10 рядків */ }
else if (order.status == Status.PAID) { /* 15 рядків */ }
else if (order.status == Status.SHIPPED) { /* 20 рядків */ }
else if (order.status == Status.DELIVERED) { /* 8 рядків */ }
else if (order.status == Status.CANCELLED) { /* 5 рядків */ }
else { throw IllegalStateException() }
}
// Виправлення: поліморфізм замість switch
interface OrderHandler {
fun handle(order: Order)
}
Автоматичний пошук запахів не відміняє код-рев'ю: статичні аналізатори знаходять тільки структурні проблеми, але не вловлюють семантичні запахи (Feature Envy, Inappropriate Intimacy). Комбінація автоматичних інструментів і людського контролю дає найкращий результат. Налаштуйте CI/CD пайплайн так, щоб білд падав при перевищенні порогів складності або довжини методу.
Рефакторинг — основний метод усунення запахів коду. Фаулер описує десятки технік рефакторингу, кожна з яких застосовна до певного запаху. Extract Method — для довгих методів, Extract Class — для великих класів, Move Method — для Feature Envy. Важливо виконувати рефакторинг маленькими кроками, зберігаючи працездатність коду після кожної зміни.
Тести перед рефакторингом — обов'язкова умова. Якщо код не покритий Unit-тестами, рефакторинг перетворюється на переписування з невідомим результатом. Для legacy-коду, де тестів немає, використовуйте Characterisation Tests — напишіть тести, які фіксують поточну поведінку, а потім рефакторте. Тестування дає впевненість, що після рефакторингу бізнес-логіка не зламалася.
Поступовість — ключ до успішного виправлення запахів у мобільній розробці. Не намагайтеся переписати God Activity цілком. Виділіть спочатку шар навігації, потім шар даних, потім логіку відображення. Кожен крок супроводжуйте комітом і прогоном тестів. Використовуйте feature toggle, щоб вмикати рефакторинг для частини користувачів і відкочувати при проблемах.
Інструменти IDE автоматизують багато технік рефакторингу. Android Studio і IntelliJ IDEA пропонують вбудовані рефакторинги: Extract Method, Extract Interface, Pull Members Up, Encapsulate Fields. Xcode (починаючи з версії 14) поліпшив підтримку рефакторингу для Swift. Використання автоматичних рефакторингів знижує ризик помилок порівняно з ручним копіюванням коду.
Мобільна розробка додає свої специфічні запахи, пов'язані з платформеними обмеженнями. В Android це витік Context, незакриті Cursor, неправильне використання Lifecycle. В iOS — retain cycle через замикання, неправильна робота з Auto Layout, гігантські ViewController. Ці запахи не тільки погіршують підтримуваність, але й безпосередньо впливають на продуктивність і стабільність застосунку.
Callback Hell — характерний запах для коду, що працює з асинхронними операціями. Вкладені колбеки (callback inside callback) роблять код нечитабельним і важко налагоджуваним. Рішення: корутини (Kotlin), async/await (Swift 5.5+), RxJava/RxSwift або Combine. За даними Google I/O 2023, проєкти, які перейшли з callback-стилю на корутини, скорочують кількість багів на 30% і прискорюють додавання нових фіч.
Platform Coupling — жорстка прив'язка бізнес-логіки до платформених компонентів. Тестування такої логіки потребує запуску емулятора, що уповільнює фідбек-цикл. Виправлення: Clean Architecture розділяє код на шари Domain (чистий Kotlin/Swift без залежностей від платформи) і Data/UI (з платформеними залежностями). Бізнес-логіка тестується на JVM без емулятора.
Часто задавані питання
Ні — Code Smell не є помилкою. Код із запахом працює коректно, але його важко підтримувати, змінювати та тестувати. Баг — це некоректна поведінка, запах — попередження про потенційні проблеми в майбутньому.
22 запахи у другому виданні книги «Refactoring» (2019). Серед них Long Method, Large Class, Primitive Obsession, Data Clumps, Switch Statements, Speculative Generality та інші. Спільнота доповнила список десятками нових запахів для сучасних парадигм і платформ.
Комбінація дає найкращий результат: Detekt (Android/Kotlin), SwiftLint (iOS), SonarQube (обидва) для автоматичного аналізу та код-рев'ю для семантичних запахів. Жоден інструмент не знаходить 100% проблем — людський досвід залишається вирішальним.
Можна, якщо код змінюється рідко або буде повністю переписаний найближчим часом. Однак накопичення запахів перетворюється на технічний борг: кожна нова зміна дається все важче, а вартість виправлення зростає експоненційно.
Так — Declarative-фреймворки породили нові запахи: гігантські @State-блоки, неправильна робота з повторними рендерами, надмірні recomposition, відсутність Extraction в окремі View. Для SwiftUI типовий запах — Massive View із десятками @State змінних.
Підсумки
Ми розробимо мобільний застосунок під ключ
IT Sectr створює застосунки для iOS та Android для стартапів і бізнесу з 2017 року. Ми проконсультуємо вас і запропонуємо найкраще рішення.
Читайте також