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

Як принцип 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 (від найгіршого до найкращого):

  1. Coincidental (Випадкова — найгірша): Елементи зібрані в клас абсолютно випадково (CommonUtils, Helper). Зв’язок відсутній.
  2. Logical (Логічна): Методи об’єднані за загальним типом операції, але роблять різні речі (клас InputHandler із методами readFromKeyboard(), readFromNetwork(), readFromTape()).
  3. Temporal (Часова): Дії виконуються в один і той самий час, але логічно не пов’язані (клас StartupManager: очистити тимчасові файли, ініціалізувати логер, перевірити ліцензію).
  4. Procedural (Процедурна): Методи викликаються в суворій послідовності для виконання алгоритму, але оперують різними даними.
  5. Communicational (Комунікативна): Методи працюють з одним і тим самим набором вхідних або вихідних даних (наприклад, OrderRepository: пошук, збереження, видалення сутності Order).
  6. Sequential (Послідовна): Вихідні дані одного методу слугують вхідними даними для наступного (конвеєр: ParseRawText $\rightarrow$ ValidateTokens $\rightarrow$ BuildAST).
  7. 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) породжує фатальну апаратну проблему:

  1. Кеш-лінія процесора (CPU Cache Line): ядро процесора завантажує пам’ять блоками по 64 байти.
  2. False Sharing (Хибне розділення): Якщо клас об’єднує кілька відповідальностей, в одному екземплярі об’єкта поруч розташовуються поля для різних завдань (наприклад, volatile long lastProcessedOrderId та volatile long emailBatchCounter).
    • Потік 1 (воркер замовлень) безперервно мутує lastProcessedOrderId.
    • Потік 2 (воркер відправки листів) безперервно мутує emailBatchCounter.
    • Незважаючи на те, що потоки працюють із логічно різними змінними, вони потрапляють в одну й ту саму 64-байтну кеш-лінію L1/L2. Протокол когерентності кешів (MESI/MOESI) змушує процесор безперервно інвалідувати кеші ядер, сповільнюючи виконання програми у 10–50 разів!
  3. Рішення через 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).

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