Питання 13 · Розділ 18

Як принцип Single Responsibility пов'язаний з cohesion

LCOM аналізує, скільки методів використовують спільні поля. Обчислюється статичними аналізаторами (SonarQube, jdepend):

Мовні версії: English Russian Ukrainian

🟢 Junior Level

Cohesion (зв’язність) — це міра того, наскільки методи і поля класу пов’язані один з одним. SRP і cohesion тісно пов’язані: коли клас має одну відповідальність (SRP), його елементи природно працюють над однією метою — це висока зв’язність.

Проста аналогія: Уявіть команду. Якщо всі працюють над одним завданням (висока зв’язність) — все відмінно. Якщо кожен робить своє (низька зв’язність) — хаос.

Приклад високої зв’язності:

// Висока cohesion: всі методи працюють з замовленнями
public class OrderService {
    public Order createOrder() { /* ... */ }
    public void cancelOrder(Order order) { /* ... */ }
    public BigDecimal calculateTotal(Order order) { /* ... */ }
}

Приклад низької зв’язності:

// Низька cohesion: методи роблять зовсім різні речі
public class UserManager {
    public void sendEmail() { /* пошта */ }
    public void calculateSalary() { /* зарплата */ }
    public void generateReport() { /* звіти */ }
    public void saveToDatabase() { /* БД */ }
}

Коли прагнути до високої cohesion:

  • При проектуванні нових класів, де відповідальність ще не усталася
  • При рефакторингу “роздутих” класів
  • Не варто гнатися за ідеальною зв’язністю в прототипах і MVP — там швидкість важливіша

🟡 Middle Level

Як це працює

Cohesion — внутрішня характеристика класу: наскільки його частини “склеєні” для спільної мети. SRP — зовнішнє правило, що вказує, де різати систему.

Зв’язок: Дотримання SRP як правило веде до високої Cohesion. Клас з однією відповідальністю природно має методи, що працюють над одним завданням. Але бувають винятки — наприклад, клас-оркестратор може координувати кілька підсистем, залишаючись в рамках однієї відповідальності.

Рівні Cohesion за Майерсом (класифікація Glenford Myers, автора книги “Reliable Software Through Composite Design”)

Рівень Опис Приклад Якість
Functional Клас робить одну річ StringTokenizer ✅ Ідеал
Sequential Методи — конвейер ImageTransformer ✅ Добре
Communicational Методи працюють з одними даними OrderRepository ✅ Нормально
Procedural Методи виконуються в порядку StartupInitializer ⚠️ Терпимо
Temporal Методи виконуються в один час ConfigLoader ⚠️ Терпимо
Utility Слабка групування CommonUtils ❌ Погано
Coincidental Випадкове групування Helper ❌ Жахливо

Метрика LCOM (Lack of Cohesion in Methods — “нестача зв’язності методів”)

LCOM аналізує, скільки методів використовують спільні поля. Обчислюється статичними аналізаторами (SonarQube, jdepend):

  • Якщо 10 методів: 5 працюють з полем A, 5 з полем B → LCOM високий → клас варто розділити на 2 класи
  • LCOM = 0: всі методи використовують спільні поля (висока зв’язність)
  • LCOM > 1.0: методи утворюють незалежні групи (низька зв’язність)

SRP і Coupling баланс

Мета: High Cohesion + Low Coupling

Підхід Cohesion Coupling Результат
God Object (один клас робить все — порушує SRP) Низька Низький ❌ Крихкий
Атомарний пил (занадто дрібні класи — сотні класів по 5 рядків) Висока Жахливий ❌ Складний
Баланс Висока Низький ✅ Ідеал

Типові помилки

Помилка Рішення
Клас з “And/Or/Manager” в імені Розділити за відповідальністю
LCOM > 1.0 Виділити окремі класи
Package-by-Layer (групування за технічними шарами: всі контролери, всі сервіси, всі репозиторії) → низька зв’язність Package-by-Feature (групування за бізнес-фічами: Order, User, Payment — кожен пакет містить свій controller + service + repository)

Коли НЕ варто гнатися за ідеальною cohesion

  • Прототипи/MVP (швидкість важливіша)
  • Маленькі утиліти (до 200 рядків)
  • Коли баланс порушується в інший бік (atomic dust)

🔴 Senior Level

Internal Implementation: LCOM4 і граф зв’язності

LCOM4 (LCOM версії 4, покращена від Henderson-Sellers) обчислюється через граф залежностей:

  • Вузли = методи + поля
  • Ребра = метод використовує поле
  • Якщо граф розпадається на N компонентів → клас варто розділити на N частин
Методи: [m1, m2, m3, m4, m5]
Поля: [f1, f2, f3, f4]

m1→f1, m2→f1, m3→f2  (Component 1)
m4→f3, m5→f4          (Component 2)

LCOM4 = 2 → розділити на 2 класи

Архітектурні Trade-offs

Strict SRP (Functional Cohesion):

  • ✅ Плюси: Ідеальна тестованість, мінімум side effects, easy refactoring
  • ❌ Мінуси: Class explosion, high coupling між класами, cognitive load

Balanced SRP (Communicational Cohesion):

  • ✅ Плюси: Баланс тестованості і navigability
  • ❌ Мінуси: Вимагає зрілого судження, gradual degradation risk

Edge Cases

  1. Anemic Domain Model (класи тільки з getter/setter, без бізнес-логіки — всі обчислення винесені в сервіси): Класи тільки з getter/setter → LCOM = 0, але це не значить “добре”
    • Рішення: DDD (Domain-Driven Design — підхід, де бізнес-логіка живе поруч з даними, а не в окремих сервісах) — логіка поруч з даними
  2. Package-by-Layer vs Package-by-Feature:
    • Layer: [Controller, Service, DAO] → низька зв’язність на рівні пакету
    • Feature: [Order, User, Payment] → висока зв’язність, SRP на рівні модулів
  3. Transaction Boundaries: Один клас координує транзакцію через кілька доменів
    • Рішення: Orchestrator pattern, але бізнес-логіка в окремих сервісах

Продуктивність

Cache Locality:

Висока cohesion:
  class OrderProcessor { fields: order, items, total }
  → Дані поруч в пам'яті → L1 cache hit rate >90%

Низька cohesion:
  class GodObject { fields: order, email, db, cache, log, ... }
  → Дані розкидані → L1 cache miss rate >30%

CPU Cache Impact: Висока cohesion → краща spatial locality → +15-30% throughput

Thread Safety

  • High cohesion classes: Зазвичай простіше синхронізувати (один lock на клас)
  • Low cohesion (God Object): Multiple responsibilities → lock contention → bottleneck

Production Experience

Рефакторинг BillingEngine (12,000 lines): LCOM4 = 8, 6 полів використовувалися різними підмножинами методів. Розділили на:

  • TaxCalculator (Functional cohesion)
  • InvoiceGenerator (Sequential cohesion)
  • PaymentProcessor (Communicational cohesion)
  • BillingOrchestrator (координатор)

Результат: LCOM4 = 1.0 для кожного, баги при деплої ↓70%.

Monitoring

ArchUnit:

@ArchTest
static void classes_should_have_high_cohesion = classes()
    .should().haveSimpleNameNotContaining("Manager")
    .andShould().haveSimpleNameNotContaining("Utils")
    .andShould().haveLessThanNDependencies(7);

SonarQube: LCOM > 1.0 → Code Smell, Cognitive Complexity > 15

jdepend: Metrics reports → Lack of Cohesion in Methods

Best Practices for Highload

  • Package-by-Feature для модульної зв’язності
  • LCOM4 < 1.0 як target метрика
  • Functional Cohesion для hot-path класів
  • ArchUnit для автоматичної перевірки

🎯 Шпаргалка для інтерв’ю

Обов’язково знати:

  • Cohesion (зв’язність) — наскільки методи класу працюють для однієї мети; SRP → висока Cohesion
  • Рівні cohesion за Майерсом: Functional (ідеал) → Sequential → Communicational → Procedural → Temporal → Utility → Coincidental (жахливо)
  • LCOM (Lack of Cohesion in Methods): LCOM = 0 → висока зв’язність, LCOM > 1.0 → варто розділити
  • LCOM4: якщо граф залежностей розпадається на N компонентів → клас варто розділити на N частин
  • Мета: High Cohesion + Low Coupling — уникати God Object і Atomic Dust
  • Package-by-Feature дає високу зв’язність, Package-by-Layer — низьку
  • Висока cohesion → краща spatial locality → +15-30% throughput (CPU cache impact)

Часті уточнюючі запитання:

  • До якого рівня cohesion прагнути? — Functional для hot-path, Communicational для більшості класів
  • LCOM4 = 2 що значить? — Клас варто розділити на 2 незалежних класи
  • Anemic Domain Model і cohesion? — Гетери/сетери → LCOM = 0, але це не “добре” — логіка винесена в сервіси
  • Що таке Atomic Dust? — Занадто дрібні класи (сотні по 5 рядків), висока зв’язність але жахливе coupling

Червоні прапори (НЕ говорити):

  • “Потрібно гнатися за ідеальною cohesion завжди” (прототипи/MVP — швидкість важливіша)
  • “LCOM = 0 завжди означає хороший дизайн” (Anemic Domain Model теж дає LCOM = 0)
  • “Package-by-Layer — найкращий підхід” (низька зв’язність на рівні пакету, краще Package-by-Feature)

Пов’язані теми:

  • [[1. Що таке принцип Single Responsibility і як його застосовувати]]
  • [[14. Що станеться, якщо клас має кілька причин для зміни]]
  • [[21. Як визначити, що клас має одну відповідальність]]
  • [[18. Як рефакторити God Object (божественний об’єкт)]]