Як принцип Single Responsibility пов'язаний з cohesion
LCOM аналізує, скільки методів використовують спільні поля. Обчислюється статичними аналізаторами (SonarQube, jdepend):
🟢 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
- Anemic Domain Model (класи тільки з getter/setter, без бізнес-логіки — всі обчислення винесені в сервіси): Класи тільки з getter/setter → LCOM = 0, але це не значить “добре”
- Рішення: DDD (Domain-Driven Design — підхід, де бізнес-логіка живе поруч з даними, а не в окремих сервісах) — логіка поруч з даними
- Package-by-Layer vs Package-by-Feature:
- Layer:
[Controller, Service, DAO]→ низька зв’язність на рівні пакету - Feature:
[Order, User, Payment]→ висока зв’язність, SRP на рівні модулів
- Layer:
- 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 (божественний об’єкт)]]