📐 Раздел 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).

Связанные темы: