Як принцип Single Responsibility пов'язаний з cohesion
У проєктуванні програмного забезпечення поняття Cohesion (згуртованість / внутрішня єдність) та SRP (Single Responsibility Principle — Принцип єдиної відповідальності) нерозривн...
🟢 Junior Level
У проєктуванні програмного забезпечення поняття Cohesion (згуртованість / внутрішня єдність) та SRP (Single Responsibility Principle — Принцип єдиної відповідальності) нерозривно пов’язані:
- Cohesion (Внутрішня згуртованість): показує, наскільки тісно методи та поля одного класу пов’язані між собою спільною метою.
- SRP (Принцип єдиної відповідальності): стверджує, що у класу повинна бути лише одна причина для зміни (одне коло бізнес-завдань або один актор).
Простий взаємозв’язок
Коли ви суворо дотримуєтеся SRP, клас вирішує одне ізольоване завдання. Усі його методи оперують одними й тими самими полями — клас автоматично отримує високу згуртованість (High Cohesion).
Якщо ж клас порушує SRP («вміє все»: зберігати в базу, слати пошту, парсити JSON і рахувати податки), його методи розпадаються на ізольовані острівці — клас має низьку згуртованість (Low Cohesion).
Приклад: Низька згуртованість проти Високої
Низька згуртованість (Low Cohesion / Порушення SRP):
// Клас-швейцарський ніж: методи не мають нічого спільного між собою
public class OrderManager {
private String databaseUrl;
private SmtpClient smtpClient;
private PdfGenerator pdfGenerator;
public void saveToDatabase(Order order) { /* використовує databaseUrl */ }
public void sendEmailConfirmation(Order order) { /* використовує smtpClient */ }
public byte[] generateInvoicePdf(Order order) { /* використовує pdfGenerator */ }
}
Висока згуртованість (High Cohesion / Дотримання SRP):
Розділяємо моноліт на сфокусовані класи:
// Згуртованість = 100%: усі методи працюють із транзакціями БД
public class OrderRepository {
private final DataSource dataSource;
public void save(Order order) { /* ... */ }
public Optional<Order> findById(UUID id) { /* ... */ }
}
// Згуртованість = 100%: тільки форматування PDF
public class OrderPdfInvoiceGenerator {
private final PdfTemplate template;
public byte[] generate(Order order) { /* ... */ }
}
🟡 Middle Level
Розмежування понять: Cohesion проти Coupling
Золоте правило архітектури програмного забезпечення — «High Cohesion, Low Coupling» (Висока згуртованість, слабке зв’язування):
| Метрика | Напрямок | Визначення | До чого прагнути |
|---|---|---|---|
| Cohesion (Згуртованість) | Всередині модуля | Наскільки елементи всередині одного класу чи пакета сфокусовані на єдиному завданні. | Максимізувати (High) |
| Coupling (Зв’язність) | Між модулями | Ступінь залежності одного класу від інших класів системи. | Мінімізувати (Low) |
[Низька Cohesion + Високий Coupling] [Висока Cohesion + Низький Coupling]
(Хаос) (Ідеал SOLID)
┌──────────────────┐ ┌──────┐ ┌──────┐
│ A B C D │ │ A B │ │ C D │
└─┬──────────────┬─┘ └──┬───┘ └──┬───┘
│ │ │ │
▼ ▼ └──────►◄─────┘
[Module 1] [Module 2] Слабкий контракт
Шкала згуртованості Гленфорда Маєрса (7 рівнів)
У класичній теорії структурного проєктування (Glenford Myers, Stevens & Constantine) виділяється 7 рівнів Cohesion (від найгіршого до найкращого):
- Coincidental (Випадкова — найгірша): Елементи зібрані в клас абсолютно випадково (
CommonUtils,Helper). Зв’язок відсутній. - Logical (Логічна): Методи об’єднані за загальним типом операції, але роблять різні речі (клас
InputHandlerіз методамиreadFromKeyboard(),readFromNetwork(),readFromTape()). - Temporal (Часова): Дії виконуються в один і той самий час, але логічно не пов’язані (клас
StartupManager: очистити тимчасові файли, ініціалізувати логер, перевірити ліцензію). - Procedural (Процедурна): Методи викликаються в суворій послідовності для виконання алгоритму, але оперують різними даними.
- Communicational (Комунікативна): Методи працюють з одним і тим самим набором вхідних або вихідних даних (наприклад,
OrderRepository: пошук, збереження, видалення сутностіOrder). - Sequential (Послідовна): Вихідні дані одного методу слугують вхідними даними для наступного (конвеєр:
ParseRawText$\rightarrow$ValidateTokens$\rightarrow$BuildAST). - Functional (Функціональна — ідеал SRP): Усі елементи класу спрямовані на виконання рівно однієї суворо визначеної функції (
SHA256Hasher,QuickSorter).
🔴 Senior Level
Математичні метрики згуртованості: LCOM1, LCOM2 та LCOM4
Для формалізації Cohesion у статичних аналізаторах (SonarQube, JArchitect) використовуються метрики сімейства LCOM (Lack of Cohesion of Methods):
Метрика LCOM1 (Chidamber & Kemerer, 1994):
Розглядає всі пари методів класу $(M_i, M_j)$.
- Нехай $P$ — кількість пар методів, що не мають спільних використовуваних полів екземпляра ($I_i \cap I_j = \emptyset$).
- Нехай $Q$ — кількість пар методів, що мають хоча б одне спільне поле ($I_i \cap I_j \neq \emptyset$). \(\text{LCOM1} = \begin{cases} P - Q, & \text{якщо } P > Q \\ 0, & \text{інакше} \end{cases}\) Чим вище $P$, тим ближче клас до порушення SRP.
Метрика LCOM4 (Hitz & Montazeri):
LCOM4 будує неорієнтований граф:
- Вузли графа: методи класу.
- Ребра графа: з’єднують два методи, якщо вони звертаються хоча б до однієї спільної змінної екземпляра або якщо один метод безпосередньо викликає інший.
- Результат LCOM4: кількість компонент зв’язності графа.
- $\text{LCOM4} = 1$: ідеальний монолітний клас (усі методи пов’язані).
- $\text{LCOM4} \ge 2$: клас розпався на незв’язані острови! Кількість компонент зв’язності показує, на скільки незалежних класів його необхідно розділити під час рефакторингу.
Компонента 1 (SRP Cluster 1) Компонента 2 (SRP Cluster 2)
[Method 1] ─── (fieldA) ─── [Method 2] [Method 3] ─── (fieldB) ─── [Method 4]
===> Граф незв'язаний (LCOM4 = 2). Порушення SRP! Потрібен поділ на 2 класи.
Апаратний вплив на CPU: False Sharing та кеш-лінії
На рівні сучасних багатоядерних процесорів низька згуртованість (Low Cohesion / God Object) породжує фатальну апаратну проблему:
- Кеш-лінія процесора (CPU Cache Line): ядро процесора завантажує пам’ять блоками по 64 байти.
- False Sharing (Хибне розділення):
Якщо клас об’єднує кілька відповідальностей, в одному екземплярі об’єкта поруч розташовуються поля для різних завдань (наприклад,
volatile long lastProcessedOrderIdтаvolatile long emailBatchCounter).- Потік 1 (воркер замовлень) безперервно мутує
lastProcessedOrderId. - Потік 2 (воркер відправки листів) безперервно мутує
emailBatchCounter. - Незважаючи на те, що потоки працюють із логічно різними змінними, вони потрапляють в одну й ту саму 64-байтну кеш-лінію L1/L2. Протокол когерентності кешів (MESI/MOESI) змушує процесор безперервно інвалідувати кеші ядер, сповільнюючи виконання програми у 10–50 разів!
- Потік 1 (воркер замовлень) безперервно мутує
- Рішення через SRP: розділення відповідальностей розносить змінні по незалежних об’єктах у пам’яті, усуваючи апаратну конкуренцію кеш-ліній.
4 Tricky Questions
1. Чи може клас володіти максимальною згуртованістю (High Cohesion), але при цьому грубо порушувати SRP?
Відповідь: Так. Класичний приклад — Anemic Domain Model Entity (анемічна сутність) або DTO з 50 полями та 100 гетерами/сетерами. Якщо клас просто зберігає дані таблиці БД, усі його гетери та сетери тісно пов’язані з його полями (формальний LCOM близький до ідеалу). Проте клас грубо порушує SRP, якщо його використовують 5 різних акторів (бухгалтер, логіст, маркетолог, аудит), і зміна формату поля для маркетингу ламає звіт бухгалтера. Висновок: Cohesion — це структурна характеристика зв’язності методів і полів у коді. SRP — це архітектурна характеристика орієнтації на актора бізнесу (джерело вимог).
2. Чи призводить сліпе максимізування Cohesion до антипатерна «Атомарний пил» (Atomic Dust / Poltergeists)?
Відповідь:
Так. Якщо довести прагнення до Functional Cohesion до абсурду і створювати на кожну окрему дію чи метод самостійний клас із 3 рядків (OrderDiscountCalculator, OrderVatCalculator, OrderTotalRoundingService), виникає антипатерн Atomic Dust.
У результаті:
- Cohesion всередині кожного класу максимальна.
- Але зв’язність усієї системи (Coupling) стає катастрофічною: щоб виконати найпростіший сценарій, сервісу потрібно інжектувати та скоординувати 20 мікрокласів.
- Баланс досягається на рівні предметної області (DDD): агрегати об’єднують пов’язані інваріанти у стійкі межі.
3. Як організація пакетів (Package-by-Layer vs Package-by-Feature) впливає на згуртованість архітектури?
Відповідь:
- Package-by-Layer (
controllers,services,repositories): має низьку згуртованість на рівні модулів. У пакетіservicesлежатьBillingService,CatalogServiceтаUserService, які не мають нічого спільного між собою, крім суфікса назви (Logical/Utility Cohesion). - Package-by-Feature (
order,billing,user): має високу згуртованість (High Cohesion). Усі класи пакета (OrderController,OrderService,OrderRepository,OrderEvents) сфокусовані на єдиній бізнес-функціональності, версіонуються спільно і можуть бути ізольовано винесені в окремий мікросервіс.
4. У чому різниця між Cohesion класу та Cohesion інтерфейсу?
Відповідь:
- Cohesion класу: оцінюється за зв’язністю реалізації методів і спільно використовуваних полів стану (
LCOM). - Cohesion інтерфейсу: в інтерфейсу немає полів стану. Його згуртованість оцінюється тим, наскільки методи інтерфейсу логічно затребувані клієнтами одночасно. Якщо інтерфейс вимагає методи
print()таscan(), а клієнти завжди викликають або лише друк, або лише сканування, такий інтерфейс має низьку згуртованість і порушує Interface Segregation Principle (ISP).
🎯 Шпаргалка для інтерв’ю
Головні тези (30 секунд на відповідь):
- Зв’язок: SRP — це причина для розділення (один актор, одна відповідальність); High Cohesion — структурний наслідок правильного SRP (методи та поля класу працюють на єдину мету).
- Формула успіху: High Cohesion + Low Coupling.
- Шкала Маєрса: Найгірша — Coincidental (випадкове звалище утиліт); Найкраща — Functional (клас вирішує рівно одну функцію).
- Метрика LCOM4: Кількість компонент зв’язності графа методів і полів. LCOM4 $\ge 2$ означає, що клас розпався на ізольовані кластери і повинен бути розділений.
- False Sharing: Low Cohesion у високонавантаженому коді розміщує різні бізнес-поля в одній 64-байтній кеш-лінії процесора, викликаючи взаємне блокування кешів CPU.
Чого уникати (Red Flags):
- ❌ Плутати Cohesion (внутрішня згуртованість модуля) та Coupling (зовнішня залежність модулів).
- ❌ Стверджувати, що Anemic Model з гетерами/сетерами завжди відповідає SRP лише тому, що в неї ідеальний LCOM.
- ❌ Захищати створення класів з 1 методом на 3 рядки для всього поспіль (це Atomic Dust / Overengineering).