Dependency Hell — ситуація, коли менеджер пакетів не може вирішити конфлікти версій бібліотек у проєкті. У мобільній розробці Dependency Hell особливо болючий: Gradle в Android і CocoaPods/SPM в iOS часто стикаються з транзитивними конфліктами. За даними звіту Sonatype (2024), середня кількість прямих залежностей у мобільному проєкті перевищує 80, а транзитивних — 400+, кожна з яких потребує сумісності версій.
Головне
Dependency Hell — термін, що описує ситуацію, коли система керування залежностями не може вирішити конфлікт версій між бібліотеками. Проєкт потребує бібліотеку A версії 1.x і бібліотеку B версії 2.x, але A залежить від C версії 1.0, а B — від C версії 2.0, при цьому C:1.0 і C:2.0 несумісні.
Проблема характерна для всіх екосистем з пакетними менеджерами. В Android — конфлікти Gradle між support library і AndroidX. В iOS — конфлікти CocoaPods між різними версіями Alamofire. В Node.js — конфлікти peer dependency npm. В Python — помилки вирішення pip.
Сучасні менеджери залежностей (npm v7+, Gradle 7+, SwiftPM) покращили алгоритми вирішення, але повне усунення конфліктів неможливе при сотнях транзитивних залежностей. Dependency Hell перейшов із категорії «помилка збірки» в категорію «управління ризиками».
Diamond dependency — класика жанру. Бібліотека A залежить від D:1.0, бібліотека B залежить від D:2.0. Якщо A і B використовуються разом, менеджер пакетів має вирішити, яку версію D встановити. У більшості випадків вибирається максимальна версія (2.0), але якщо A не сумісна з D:2.0 — конфлікт нерозв'язний.
Version conflict — явна невідповідність вимог. A потребує Logging >=2.0, B потребує Logging <2.0. Менеджер не може задовольнити обидві умови. Peer dependency conflict — плагін A потребує React 17, але проєкт використовує React 18 з радикальними змінами. npm виводить попередження, але встановлення проходить — поведінка стає непередбачуваною.
Transitive dependency hell — коли залежність не пряма, а опосередкована. Розробник не знає, що бібліотека A залежить від B, а B — від C. Gradle Dependency Tree — інструмент для візуалізації всього ланцюжка залежностей, що показує, звідки береться конфліктуюча бібліотека.
Circular dependency — A залежить від B, а B залежить від A. Сучасні менеджери (Gradle, npm) блокують циклічні залежності на етапі збірки. Рішення — виділення спільного модуля C, від якого залежать і A, і B, розриваючи цикл.
Зростання кількості бібліотек — основна передумова. Кожен модуль додає прямі та транзитивні залежності. В Android-проєкті на Jetpack Compose з Firebase, Retrofit і Coil кількість транзитивних залежностей легко перевищує 500. Кожна нова бібліотека — потенційний конфлікт.
Несинхронізовані оновлення — команди оновлюють бібліотеки в різний час. Бекенд оновлює Jackson до 2.15, Analytics-команда використовує 2.12. При інтеграції модулів виникає конфлікт. Рішення — централізовані версії (Bill of Materials) у BOM-файлі Gradle або каталозі версій.
Різні версії однієї бібліотеки — класична ситуація: модуль A використовує OkHttp 3.12, модуль B — OkHttp 4.0. Якщо оновлення до 4.0 ламає модуль A, проєкт застряє на двох версіях, що може призвести до конфліктів classpath у Java або дублікатів символів в iOS.
Gradle Dependency Tree — команда `gradle dependencies` виводить повне дерево залежностей із зазначенням конфліктів. Вирішена версія показує, яку версію Gradle вибрав, а конфліктні версії позначені стрілками. Приклад: `com.squareup.okhttp3:okhttp -> 4.9.3 (*)` — версію вирішено, (*) — дублювання.
npm ls — аналогічна команда для Node.js. Прапорець `--all` показує повне дерево. Конфлікти peer dependency виводяться з попередженнями. SwiftPM Graph — `swift package show-dependencies` показує граф залежностей для iOS-проєктів, включаючи гілки та ревізії.
Dependency Analysis Plugin — Gradle-плагін від Autonomy, який знаходить невикористовувані залежності та конфлікти. Ben Manes Versions Plugin — перевіряє, які залежності застаріли, і показує доступні оновлення. Обидва інструменти автоматизують рутинну перевірку сумісності.
// Конфлікт: модуль A потребує okhttp 3.x, модуль B потребує okhttp 4.x
dependencies {
implementation("com.example:module-a:1.0") // -> okhttp 3.12
implementation("com.example:module-b:2.0") // -> okhttp 4.0
}
// Рішення: примусова конкретної версії
configurations.all {
resolutionStrategy {
force "com.squareup.okhttp3:okhttp:4.9.3"
}
}
Version Catalog (Gradle 7+) — централізоване оголошення версій у TOML-файлі. Всі модулі використовують одні й ті самі версії бібліотек. Приклад: файл `libs.versions.toml` містить `okhttp = «4.9.3»`, і всі модулі посилаються на цей каталог. Конфлікт версій між модулями виключається.
Bill of Materials (Spring BOM) — Maven-концепція, при якій задаються сумісні версії бібліотек. Команда Google Android використовує Compose BOM для бібліотек Jetpack. Підключивши BOM, ти отримуєш гарантію, що всі версії Compose сумісні між собою.
Renovate і Dependabot — автоматичні творці PR на оновлення залежностей. Renovate групує сумісні оновлення, перевіряє зміни через Docker-образи. Dependabot — вбудоване рішення GitHub, яке оновлює залежності та перевіряє сумісність через CI.
Semantic Versioning — використовуй caret `^1.2.3` для patch/minor оновлень і tilde `~1.2.3` для лише patch. Але навіть semver не гарантує сумісність — реальні порушення semver трапляються в 15% випадків (за даними дослідження Університету Люксембургу, 2024). Lock-файли фіксують точну версію, яка пройшла тести.
Мінімізація залежностей — кожна бібліотека має бути виправдана. Якщо можна реалізувати функціональність 20 рядками власного коду — не підключай бібліотеку. Приклад: замість бібліотеки для форматування дат (4 транзитивні залежності) використовуй вбудовані засоби платформи. Правило «бюджет залежностей» — не більше 50 прямих залежностей на проєкт.
Регулярні оновлення — оновлюй залежності малими кроками, а не раз на рік. Dependabot створює PR на кожне оновлення. CI має запускати повний набір тестів. DevContainer — єдине середовище розробки, в якому версії залежностей відповідають продакшену, виключаючи конфлікти між середовищами.
Часті запитання
Насамперед запусти `gradle dependencies` (Gradle), `npm ls` (Node.js) або `swift package show-dependencies` (SwiftPM). Знайди конфліктуючу бібліотеку. Три варіанти вирішення: примусова версія через resolutionStrategy, виключення транзитивної залежності (`exclude group:`), або оновлення однієї з конфліктуючих бібліотек до сумісної версії.
Version Catalog (libs.versions.toml) — єдине джерело істини для версій усіх бібліотек. Всі модулі проєкту посилаються на один каталог. Коли бібліотека оновлюється, версія змінюється в одному місці. Це виключає ситуацію, коли два модулі використовують різні версії однієї бібліотеки.
Транзитивні залежності — це бібліотеки, які тягне за собою пряма залежність. Розробник часто не знає про них. Небезпека: транзитивна залежність може конфліктувати з іншою прямою залежністю. Рішення — регулярно перевіряти дерево залежностей і підключати тільки ті бібліотеки, у яких мінімальна кількість транзитивних залежностей.
Не обов'язково кожен спринт, але регулярно — так. Рекомендація: раз на місяць запускай Dependabot або Renovate для створення PR. Критичні виправлення безпеки оновлюй протягом тижня. Незначні оновлення — в межах звичайного спринту. Великі оновлення потребують окремої оцінки змін.
Бібліотека без підтримки — ризик безпеки та сумісності. Стратегія: знайди альтернативу з активною спільнотою (зірки GitHub, дата останнього коміту), сплануй міграцію через абстракцію (Interface/Protocol), заміни бібліотеку за 2–3 спринти. Якщо альтернативи немає — форкни репозиторій і підтримуй версію всередині команди.
Підсумки
Ми розробимо мобільний застосунок під ключ
IT Sectr створює застосунки для iOS та Android для стартапів і бізнесу з 2017 року. Ми проконсультуємо вас і запропонуємо найкраще рішення.
Читайте також