Cohesion (зв'язність) — це метрика, яка показує, наскільки тісно пов'язані елементи всередині одного модуля чи класу. За даними Wikipedia, висока зв'язність є ознакою добре спроєктованого модуля, де всі методи та поля працюють над одним завданням. Cohesion безпосередньо впливає на підтримуваність коду та протиставляється coupling — зв'язаності між модулями.
Головне
Cohesion (зв'язність, зчеплення) — метрика, яка оцінює, наскільки методи, поля та властивості всередині одного класу чи модуля логічно пов'язані між собою. Високозв'язний модуль виконує одне завдання та містить лише ті елементи, які необхідні для його виконання. Низькозв'язний модуль намагається робити кілька речей одночасно — методи слабко пов'язані за змістом.
У контексті об'єктно-орієнтованого програмування cohesion тісно пов'язаний із Single Responsibility Principle (S). Якщо клас має одну чітку відповідальність, його cohesion, як правило, високий. Якщо клас займається і UI, і бізнес-логікою, і роботою з мережею — cohesion низький, і такий клас варто розбити на кілька окремих класів із вужчою відповідальністю.
Розуміння cohesion допомагає розробнику приймати рішення про рефакторинг. Коли ви бачите, що в класі є метод, який не використовує поля класу, це сигнал низької зв'язності. Такий метод або зайвий у класі, або клас неправильно спроєктовано. Прагнення до високої зв'язності — це постійна робота з покращення архітектури на кожному рівні коду.
У програмній інженерії виділяють сім рівнів cohesion, розташованих від найгіршого до найкращого. Розуміння цієї шкали дозволяє об'єктивно оцінити якість модуля та зрозуміти, в якому напрямку рухатися при рефакторингу. Чим вищий рівень, тим більш підтримуваним і зрозумілим буде код.
Випадкова (coincidental) — найгірший рівень, коли елементи в модулі згруповані випадково, без будь-якого логічного зв'язку. Приклад: клас Utilities, у якому зібрані методи форматування дати, надсилання email та розрахунку знижки. Такий клас неможливо зрозуміти без читання всіх методів, і зміна одного методу може зламати інші лише тому, що вони знаходяться поруч.
Логічна (logical) зв'язність — елементи виконують логічно пов'язані, але різні за суттю завдання. Клас із методами parseJSON, parseXML і parseCSV логічно пов'язаний темою «парсинг», але кожен метод робить принципово різну роботу. Проблема: при додаванні нового формату (YAML) клас зростає, і його інтерфейс стає роздутим.
Часова (temporal) зв'язність — елементи згруповані за часом виконання. Клас AppInitializer, який налаштовує базу даних, завантажує конфіг, ініціалізує аналітику — все це відбувається при запуску застосунку, але самі завдання не пов'язані між собою. Краще розділити їх на окремі Initializer для кожної зони відповідальності.
Процедурна (procedural) зв'язність виникає, коли елементи об'єднані послідовністю виконання. Модуль «Обробка замовлення» містить методи validateCart, processPayment, sendConfirmation — кожен метод викликається строго після попереднього. Це краще випадкової або логічної зв'язності, але все ще не ідеал: кожен крок може бути винесений в окремий модуль.
Комунікаційна (communicational) зв'язність — елементи працюють з одними й тими самими даними. Клас UserService з методами getUser, updateUser, deleteUser об'єднаний спільною сутністю User. Це суттєво краще процедурної зв'язності: клас має чітку предметну область. Більшість Repository класів у мобільних проєктах мають комунікаційну зв'язність.
Функціональна (functional) зв'язність — найвищий рівень, коли кожен елемент модуля бере участь у виконанні одного завдання. Клас PasswordValidator з єдиним методом validate, який перевіряє довжину, наявність символів і складність пароля — приклад функціональної зв'язності. Якщо такий клас змінюється, то тільки тому, що змінилися правила валідації паролів.
Досягнення функціональної зв'язності — основна мета архітектурного рефакторингу. Кожен клас повинен мати рівно одну причину для зміни. У мобільній розробці функціональна зв'язність досягається через виділення окремих Use Cases, кастомних View, форматувальників і валідаторів. Кожен такий клас — закінчений будівельний блок із чіткою зоною відповідальності.
Cohesion і coupling — дві сторони однієї якості. Чим вищий cohesion всередині модуля, тим, як правило, нижчий coupling між модулями. Добре спроєктована система одночасно прагне до високої зв'язності всередині та слабкої зв'язаності ззовні. Це правило визнається фундаментальним у програмній інженерії з 1970-х років.
Співвідношення cohesion-coupling можна представити як баланс. Якщо розробник жертвує cohesion, об'єднуючи в одному класі кілька завдань, сусідні модулі отримують більше залежностей — їм доводиться звертатися до цього перевантаженого класу для різних цілей, що збільшує coupling. І навпаки, розбиття на дрібні високозв'язні класи зменшує кількість точок взаємодії між модулями.
На практиці це означає: коли ви виділяєте новий клас із функціональною зв'язністю, ви одночасно позбавляєте інші модулі необхідності знати деталі його реалізації. Наприклад, виділивши EncryptionManager в окремий клас із функціональною зв'язністю, ви даєте іншим модулям простий інтерфейс encrypt/decrypt без необхідності розуміти деталі алгоритму шифрування.
// Низька зв'язність — клас робить все одразу
class UserManager {
fun fetchAndSaveUser(id: String) { }
fun parseUserJson(json: String): User { }
fun displayUserName(user: User): String { }
fun validateEmail(email: String): Boolean { }
}
// Висока зв'язність — кожен клас вирішує одне завдання
class UserRepository {
fun fetchUser(id: String): User { }
}
class UserJsonParser {
fun parse(json: String): User { }
}
class UserNameFormatter {
fun format(user: User): String { }
}
class EmailValidator {
fun isValid(email: String): Boolean { }
}
Приклад показує різницю: UserManager має логічну зв'язність — всі методи про користувачів, але кожен робить принципово різну роботу. Після рефакторингу кожен клас має функціональну зв'язність, а coupling знижується, тому що інші модулі залежать тільки від потрібного їм класу, а не від всього UserManager.
LCOM (Lack of Cohesion of Methods) — найвідоміша метрика для вимірювання зв'язності класу. LCOM підраховує, скільки пар методів не використовують спільні поля. Значення 0 означає ідеальну зв'язність (всі методи працюють з одними полями), високе значення — низьку зв'язність. LCOM4 (вдосконалена версія) враховує транзитивні зв'язки через інші методи.
У Android-розробці метрики зв'язності можна отримати через Detekt із правилом TooManyFunctions. Класи з десятком методів, які використовують різні групи полів, швидше за все мають низьку зв'язність. В iOS SwiftLint має правило file_length і function_body_length — непрямі індикатори: довгі файли та методи часто сигналізують про низький cohesion.
Ручний спосіб оцінки: поставте запитання «Цей клас зміниться з однієї причини чи з кількох?» Якщо ви можете назвати більше однієї незалежної причини — клас має низьку зв'язність. Другий тест: «Чи можна розділити цей клас на два незалежних класи?» Якщо так — робіть це. Регулярна перевірка cohesion на код-рев'ю запобігає появі God класів і знижує технічний борг.
Перший крок — застосувати Single Responsibility Principle. Кожен клас повинен мати одну чітку відповідальність. Якщо в класі є метод, який не відноситься до його основного завдання, винесіть його в окремий клас. Техніка Extract Class або Extract Delegate в IDE автоматизує цей процес. Після виділення перевірте, чи став вихідний клас більш сфокусованим.
Другий крок — використовувати паттерн Facade для спрощення інтерфейсу. Якщо клас надає 20 методів, з яких клієнти використовують лише 3-4, можливо, у класу низька зв'язність — він надає занадто багато різної функціональності. Згрупуйте методи за темами та виділіть окремі класи для кожної групи, а вихідний клас зробіть фасадом або видаліть.
Третій крок — звернути увагу на групи полів. Якщо в класу є поля, які використовуються лише частиною методів — це індикатор низького cohesion. Розділіть клас за групами полів. Наприклад, якщо клас містить поля userRepository, networkClient і analyticsTracker, але методи першої групи використовують тільки userRepository, а другої — networkClient — це два різних класи.
Четвертий крок — уникати створення «утилітних» класів із довільними static методами. Кожен static метод, що знаходиться в класі Utils або Helpers — кандидат на виділення в спеціалізований клас. FormatUtils.dateToString краще перенести в DateFormatter, а ValidationUtils.isValidEmail — в EmailValidator. Це підвищує cohesion кожного класу та робить код самодокументованим.
Поширені запитання
Майже завжди. Функціональна зв'язність робить код зрозумілим і передбачуваним. Однак доведення до крайності може призвести до надмірного дроблення: коли для кожної операції створюється окремий клас, і архітектура стає надмірно складною. Баланс — кілька класів на фічу, кожен із функціональною зв'язністю.
Cohesion — метрика внутрішньої узгодженості одного модуля або класу. Modularity — архітектурний принцип, при якому застосунок розбивається на фізичні модулі. Високий cohesion є метою при проєктуванні як окремих класів, так і цілих модулів.
Detekt для Android і Xcode Analyzer для iOS підсвічують класи з підозріло великою кількістю методів або полів. IntelliJ IDEA і AppCode мають візуалізацію залежностей — ви можете побачити граф зв'язків і виявити класи з низькою зв'язністю. SonarQube рахує метрики LCOM автоматично.
Так. Інтерфейс із методами connect, disconnect і isConnected має високу зв'язність — всі методи відносяться до управління з'єднанням. Інтерфейс із методами connect, parseData і renderUI має низьку зв'язність. Принцип Interface Segregation (SOLID) вимагає створювати вузькоспеціалізовані інтерфейси з високою зв'язністю.
Задайте три запитання: чи можна описати призначення класу одним реченням? Чи всі методи підтримують це призначення? Чи є в класі поля, які не використовуються частиною методів? Якщо відповідь на будь-яке запитання негативна — cohesion низький, і клас варто розділити.
Підсумки
Ми розробимо мобільний застосунок під ключ
IT Sectr створює застосунки для iOS та Android для стартапів і бізнесу з 2017 року. Ми проконсультуємо вас і запропонуємо найкраще рішення.
Читайте також