Зв'язність (Coupling) — це метрика, яка показує, наскільки один модуль застосунку залежить від іншого. За даними Wikipedia, слабка зв'язність (low coupling) — ознака добре спроєктованої системи, де модулі можна змінювати, не ламаючи сусідні. Керування зв'язністю — одне з головних завдань архітектора під час проєктування мобільних застосунків.
Головне
Coupling (зв'язність, зчеплення) — це метрика, що визначає, наскільки сильно один модуль або клас пов'язаний з іншим. Чим більше один модуль знає про внутрішній устрій іншого, тим вища зв'язність і тим складніше змінювати систему. У добре спроєктованій архітектурі зв'язність має бути мінімальною — модулі взаємодіють лише через чітко визначені інтерфейси.
Розрізняють дві сторони зв'язності: аферентну (вхідні залежності — скільки модулів залежать від даного) і еферентну (вихідні залежності — від скількох модулів залежить даний). Аналіз цих метрик дозволяє виявити гарячі точки в архітектурі, де зміна одного модуля вплине на безліч інших. Інструменти на кшталт IntelliJ Dependency Analyzer та Xcode Graph візуалізують ці зв'язки.
Важливо розуміти, що нульова зв'язність неможлива — модулі повинні якось взаємодіяти, інакше це не система, а набір ізольованих програм. Завдання архітектора — зробити зв'язність керованою та прозорою. Ідеал: модулі взаємодіють лише через інтерфейси та передають лише прості дані, не знаючи про внутрішній устрій одне одного. Це називається слабкою зв'язністю (loose coupling).
Шість типів зв'язності утворюють шкалу від найкращого до найгіршого. Розуміння цієї шкали допомагає оцінити існуючий код і вибрати напрямок рефакторингу. Більшість мобільних проєктів мають змішані типи зв'язності, і завдання архітектора — послідовно замінювати сильні типи на слабкі.
Data coupling (зв'язність за даними) — модулі обмінюються лише простими даними через параметри методів. Модуль А викликає метод модуля B, передаючи примітиви або прості структури, та отримує результат. Модуль А не знає, як B реалізований всередині. Це найбажаніший тип зв'язності: він мінімізує наслідки змін.
Приклад: EmailValidator.isValid(email: String): Boolean. Клас-споживач передає рядок та отримує Boolean, не маючи уявлення про регулярні вирази чи правила валідації всередині валідатора. Зміна логіки валідації не потребує зміни споживача — зв'язність мінімальна. Data coupling — мета для всіх публічних інтерфейсів у застосунку.
Stamp coupling (зв'язність за структурою) — модулі обмінюються складовими об'єктами, але використовують лише частину їхніх полів. Модуль А передає об'єкт User у метод calculateDiscount, який використовує лише user.status. Проблема: якщо структура User зміниться (додасться обов'язкове поле), модуль calculateDiscount не зміниться, але споживач, який створює об'єкт User, — зміниться.
На практиці stamp coupling неминучий і прийнятний, якщо переданий об'єкт є стандартною моделлю даних (Entity). Проблема виникає, коли модуль отримує цілий об'єкт лише заради одного поля. У таких випадках краще передавати конкретне значення безпосередньо (data coupling). Рішення — аналізувати використання полів приймаючою стороною.
Control coupling — один модуль передає іншому прапорець, що керує його поведінкою (calculate(useNewAlgorithm: Boolean)). Це гірше за stamp coupling, тому що модуль-споживач повинен знати внутрішні варіанти роботи викликаного модуля. Рішення: розділити метод на два — calculateWithNewAlgorithm() та calculateWithLegacyAlgorithm().
External coupling — модулі залежать від зовнішнього протоколу, формату даних або API. Усі модулі, які парсять один JSON або працюють з однією базою даних, мають external coupling. Повністю уникнути його не можна, але можна ізолювати: створити шар маппінгу між зовнішнім форматом і внутрішніми моделями. Common coupling — модулі розділяють спільний глобальний стан. Content coupling — найгірший тип, коли модуль безпосередньо змінює внутрішні дані іншого модуля.
| Тип зв'язності | Рівень | Опис |
|---|---|---|
| Data | Найкращий | Передача простих даних через параметри |
| Stamp | Прийнятний | Передача об'єктів із частковим використанням |
| Control | Середній | Керування поведінкою через прапорці |
| External | Високий | Залежність від зовнішнього протоколу/формату |
| Common | Дуже високий | Розділення глобального стану |
| Content | Неприпустимий | Пряма зміна внутрішніх даних модуля |
Шкала зв'язності від data (ідеал) до content (катастрофа) — практичний інструмент для код-рев'ю. Якщо ви бачите в проєкті common або content coupling — це першочергова мета рефакторингу. Data та stamp coupling прийнятні й присутні в будь-якому проєкті, але їхня кількість має контролюватися.
Висока зв'язність перетворює розробку на сповільнений процес, де кожна зміна потребує перевірки десятків потенційно зламаних модулів. У мобільній розробці це особливо критично: платформи оновлюються щорічно (Android API Level, iOS SDK), бібліотеки — щокварталу, а вимоги бізнесу — безперервно. Слабка зв'язність — єдиний спосіб справлятися з цим потоком змін без постійних регресій.
Приклад із практики: мобільний застосунок, де всі екрани безпосередньо імпортують NetworkingManager та DatabaseManager. При заміні HTTP-клієнта з Retrofit на Ktor (Android) або з URLSession на Alamofire (iOS) розробнику довелося б правити кожен екран. При низькій зв'язності достатньо змінити одну реалізацію, приховану за інтерфейсом NetworkDataSource, — споживачі не помітять заміни.
Вплив зв'язності на unit-тестування також величезний. Клас із високою зв'язністю (пряме створення залежностей через конструктор) неможливо протестувати ізольовано — він тягне за собою базу даних, мережу та UI. Для тестування такого класу доводиться підіймати емулятор і чекати інтеграційних тестів. Клас із низькою зв'язністю приймає залежності через constructor injection і легко мокається.
// Високий coupling — клас сам створює свої залежності
class ProfileViewModelHigh {
private val api = RetrofitApi()
private val db = RoomDatabase.getInstance()
private val cache = MemoryCache()
}
// Низький coupling — залежності передаються через конструктор
class ProfileViewModelLow(
private val api: ApiService,
private val db: DatabaseService,
private val cache: CacheService
)
У першому випадку ProfileViewModelHigh жорстко прив'язаний до конкретних реалізацій — заміна Retrofit на Ktor потребує зміни коду ViewModel. У другому випадку ProfileViewModelLow залежить лише від інтерфейсів, реалізації яких підставляються ззовні. Тестування другого класу тривіальне: передаємо mock-реалізації та перевіряємо логіку без емулятора.
Dependency Inversion Principle (D в SOLID) — основа для зниження зв'язності. Принцип наказує залежати від абстракцій, а не від конкретних реалізацій. Замість того щоб клас безпосередньо створював об'єкт RetrofitApi, він має отримувати інтерфейс ApiService. Це переміщує зв'язок із конкретної бібліотеки на рівень абстракції, яку можна замінити без зміни споживача.
Observer pattern (або його реактивні версії — StateFlow, Combine Publishers) знижує зв'язність між джерелом даних і підписниками. Підписник не знає, звідки беруться дані, — він просто реагує на зміни. Це розв'язує відправника й отримувача: можна додати нове джерело даних, не змінюючи існуючих підписників. EventBus та SharedFlow працюють за тим же принципом.
Bridge pattern розділяє абстракцію та реалізацію, дозволяючи їм змінюватися незалежно. У мобільній розробці Bridge застосовується, наприклад, для платформозалежних модулів: спільний інтерфейс ImageLoader з різними реалізаціями для iOS (Kingfisher, Nuke) та Android (Glide, Coil). Код, що працює з ImageLoader, не залежить від обраної бібліотеки та може її замінити простою зміною імплементації.
Dependency Injection (DI) — найбільш практичний інструмент зниження зв'язності в мобільній розробці. Замість того щоб клас самостійно створював свої залежності, DI-контейнер (Hilt, Koin, Dagger для Android; Swinject, Factory для iOS) надає їх ззовні. Клас отримує залежності через constructor, method або property injection, залишаючись у невіданні про конкретні реалізації.
DI явно документує залежності класу: достатньо поглянути на конструктор, щоб зрозуміти, з якими модулями взаємодіє клас. Якщо конструктор приймає 8 параметрів із різних шарів — це сигнал надмірної зв'язності, що потребує рефакторингу. Хороший тон — не більше 3-4 залежностей на клас. Більша кількість вказує на порушення Single Responsibility та надмірну зв'язність.
DI також спрощує тестування: для кожного тесту ви створюєте клас із мок-залежностями, не потребуючи реальної бази даних або мережі. У Flutter DI реалізується через Provider, Riverpod або GetIt. Незалежно від фреймворку, мета одна: послабити зв'язність між модулями, зробивши залежності явними та замінними. Застосування DI в мобільному проєкті — де-факто стандарт із 2020-х років.
// DI-контейнер збирає граф залежностей
protocol AuthServiceProtocol {
func login(email: String, password: String) async throws -> User
}
final class AuthService: AuthServiceProtocol {
func login(email: String, password: String) async throws -> User {
// реалізація
}
}
// ViewModel не знає про конкретний сервіс — тільки протокол
final class LoginViewModel {
private let auth: AuthServiceProtocol
init(auth: AuthServiceProtocol) {
self.auth = auth
}
}
// DI Container — єдине місце, де створюються конкретні типи
final class DIContainer {
lazy var authService: AuthServiceProtocol = AuthService()
lazy var loginViewModel: LoginViewModel {
LoginViewModel(auth: self.authService)
}
}
Тут LoginViewModel залежить лише від протоколу AuthServiceProtocol, а не від конкретного AuthService. Заміна реалізації (наприклад, перехід із Firebase Auth на власний сервер) потребує змін лише в DIContainer. Всі споживачі AuthServiceProtocol залишаються незайманими — зв'язність зведена до мінімуму через абстракцію та DI.
Часті запитання
Згуртованість вимірює внутрішню узгодженість модуля, зв'язність — зовнішню пов'язаність між модулями. Хороша архітектура прагне до високої згуртованості та низької зв'язності. Ці метрики обернено пропорційні: підвищення згуртованості зазвичай знижує зв'язність, і навпаки.
Data та stamp — норма і присутні в будь-якому проєкті. Control coupling допустимий в обмежених сценаріях (наприклад, strategy pattern). External coupling неминучий при роботі із зовнішніми API, але має бути ізольований за шаром маппінгу. Common та content coupling — ознаки архітектурних проблем, що потребують негайного рефакторингу.
Інструменти статичного аналізу: IntelliJ IDEA Dependency Matrix, Xcode Graph, Gradle Dependencies report, SonarQube. Метрики: afferent coupling (Ca), efferent coupling (Ce), нестабільність (Ce/(Ca+Ce)). Висока нестабільність (близька до 1) означає, що модуль легко змінити і на нього мало хто посилається — це добре.
Наднизька зв'язність може означати надмірну кількість абстракцій та інтерфейсів, які ускладнюють навігацію кодом. Якщо для кожного класу створено окремий інтерфейс, програміст витрачає час на стрибки між файлами. Баланс: інтерфейси для зовнішнього API модуля, але не для кожного внутрішнього допоміжного класу.
Використовуйте техніку Strangler Fig — поступово замінюйте прямі виклики через інтерфейси. Почніть із extract interface для класів, до яких звертаються найчастіше. Потім впровадьте DI-контейнер. Покривайте ізольований код Characterisation-тестами, щоб переконатися, що рефакторинг не змінює поведінку системи.
Підсумки
Ми розробимо мобільний застосунок під ключ
IT Sectr створює застосунки для iOS та Android для стартапів і бізнесу з 2017 року. Ми проконсультуємо вас і запропонуємо найкраще рішення.
Читайте також