Вопрос 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 (божественный объект)]]