Cohesion
Cohesion измеряет фокус модуля: изменяются ли его части по одной причине? Functional cohesion — цель; coincidental — авария. LCOM сигнализирует о несвязных кластерах. Cohesion — противовес чрезмерному decoupling.
Billing-инженер на B2B SaaS-платформе пытался разобраться в классе BillingUtils. Он содержал: formatCurrency(), parseInvoiceXML(), sendEmailNotification(), retryWithBackoff(), calculateTax(), validateIBAN() и archiveOldInvoices(). Класс вырастал два года, пока разработчики складывали в него «billing-связанные» утилиты в самый удобный контейнер. Каждая функция работала. Ни одна из них не принадлежала рядом с другими ни в каком осмысленном смысле — они разделяли папку, а не причину для изменения. Когда платёжный процессор сменил API и инженер искал все места форматирования валюты, пришлось прочитать весь класс, чтобы убедиться, что нашёл их все. У класса не было cohesion, и её отсутствие делало невозможным независимое рассуждение о нём.
Спектр cohesion
Yourdon и Constantine определили cohesion по спектру от случайной (самая слабая) до функциональной (самая сильная), как они определили лестницу coupling. Уровни описывают, почему элементы модуля сгруппированы вместе.
Coincidental cohesion (самая слабая): элементы сгруппированы без структурной причины — они случайно оказались в одном файле или классе. BillingUtils из открытой истории — пример случайной cohesion. Единственное, что связывает её функции — разработчик решил там их разместить. Любой рефакторинг требует чтения всего модуля, чтобы понять, что в нём есть.
Logical cohesion: элементы сгруппированы, потому что логически похожи — все обработчики ошибок, все форматтеры строк, все функции валидации. Класс StringUtils с trim, truncate, toSlug и escapeHtml имеет логическую cohesion. Они разделяют общую область (строки), но не обязательно изменяются вместе или служат одной цели.
Temporal cohesion: элементы сгруппированы, потому что выполняются в одно время — вся логика инициализации при запуске, вся логика очистки при завершении. Функции выполняются вместе, но могут не иметь ничего общего в остальном.
Communicational cohesion: элементы сгруппированы, потому что работают с одними данными. Все функции, работающие с объектом Order, образуют communicationally cohesive группу — но единый класс Order с десятками методов для совершенно разных задач (pricing, shipping, fulfillment, billing, audit) по-прежнему слабо cohesive, если эти задачи изменяются по несвязанным причинам.
Functional cohesion (самая сильная): все элементы вносят вклад в единую, чётко определённую цель. Модуль делает одно дело, все его части служат этому одному делу, и изменение этого одного дела требует изменения только этого модуля. InvoiceGenerator, принимающий подтверждённый заказ и создающий отформатированный инвойс с корректным налогом и итогами — и больше ничего — имеет функциональную cohesion. Когда формат инвойса изменяется — изменяешь этот модуль. Когда меняется налоговое законодательство — изменяешь расчёт налогов в этом модуле. Когда меняется статус платежа — этот модуль не трогаешь.
LCOM: структурная интуиция для cohesion
Lack of Cohesion of Methods (LCOM) — статическая метрика, дающая структурное ощущение того, насколько хорошо методы класса принадлежат вместе. Интуиция проста: если класс имеет методы A, B и C, и методы A и B разделяют поля экземпляра, а метод C не разделяет ни одного поля ни с одним из них — класс, вероятно, делает два дела. Метод C, скорее всего, принадлежит в другое место.
Наиболее полезная форма (LCOM4 или вариант в обычных инструментах статического анализа) считает количество связных компонентов в графе, где узлы — методы, а рёбра соединяют методы, разделяющие хотя бы одно поле экземпляра. LCOM = 1 — идеал: каждый метод достижим от каждого другого через общие поля — весь класс одна тесно связная вещь. LCOM > 1 означает, что класс имеет несколько независимых кластеров, которые, вероятно, должны быть отдельными модулями.
В B2B-платформе представь класс BillingProcessor с такими связями метод/поле:
generateInvoice()иcalculateTax()оба используютorderItemsиcustomerIdsendEmailNotification()иformatEmailBody()оба используютemailAddressиemailTemplate- Ни один метод первой группы не разделяет ни одного поля со второй группой
LCOM = 2. Класс делает две несвязанные вещи: генерацию инвойсов и отправку email. Это должны быть два класса.
▸lesson.inset.note
LCOM — диагностика, а не приговор. Класс с LCOM = 2 может намеренно группировать две вещи, которые всегда изменяются вместе по бизнес-причинам — это осознанное решение, а не дефект. Используй LCOM для выявления кандидатов на ревью, а не для автоматического рефакторинга. Вопрос, который он задаёт: «принадлежат ли эти части действительно вместе?» — ответ требует суждения человека о границах изменений домена.
Cohesion как противовес избыточному decoupling
Предыдущий урок установил, что coupling следует минимизировать — конкретно, степень, в которой один модуль зависит от внутренностей другого. Но в крайности «минимизировать coupling» производит модули настолько мелкозернистые, что ни один модуль не имеет собственного содержания. Пятьдесят классов, каждый с одним методом. Каждая операция требует компоновки пятнадцати классов. У кода data coupling везде и он полностью нечитаем.
Cohesion — противодействующая сила. Высоко cohesive модуль держит вместе то, что изменяется вместе — что точно и определяет хорошую структурную границу. Когда всё в модуле изменяется по одной причине, модуль — стабильная единица эволюции. Когда части модуля изменяются по разным причинам, модуль — неправильная граница.
Практическая связь: высокая cohesion естественно снижает coupling-поверхность. Высоко cohesive InvoiceGenerator предоставляет чистый, минимальный интерфейс (data coupling снаружи), потому что всё, что ему нужно внутренне, уже инкапсулировано. Тебе не нужно заглядывать в его внутренности, потому что все его внутренности скоординированы на одну цель. Низкая cohesion, напротив, вынуждает вызывающих знать о внутренней структуре модуля — потому что модулю не на что опереться.
▸Почему это работает
Почему cohesion важнее, чем кажется? Потому что стоимость низкой cohesion платится не в момент написания кода. Класс BillingUtils из открытой истории работал отлично. Каждая функция возвращала правильный результат. Стоимость появляется только при необходимости что-то изменить: нельзя определить, что есть в модуле, не прочитав всё; нельзя протестировать подмножество, не понимая остального; нельзя развивать одну задачу, не рискуя случайным столкновением с другой. Низкая cohesion — невидимый долг: он накапливается в момент изменения, а не в момент написания.
Модуль содержит три функции: `processPayment()`, `logPaymentAttempt()` и `notifyFinanceTeam()`. Каждая функция запускается при обработке платежа и каждая модифицирует одну запись платежа. Какая форма cohesion лучше всего описывает этот модуль?
Статический анализ показывает, что класс BillingProcessor имеет LCOM = 3. Методы, генерирующие инвойсы, разделяют поля между собой; методы, отправляющие уведомления, разделяют поля между собой; методы, архивирующие старые записи, разделяют поля между собой — но никакие поля не разделяются между тремя группами. Что это означает и что следует делать?
Команда разбивает класс InvoiceGenerator на двенадцать однометодных классов для достижения максимального слабого coupling. Теперь генерация инвойса требует координации двенадцати вызовов в правильном порядке. Какую структурную проблему они создали?
- 01Что такое функциональная cohesion и чем она отличается от communicational cohesion?
- 02Что измеряет LCOM и что означает LCOM > 1?
- 03Почему «максимизировать decoupling везде» — ловушка, и что её исправляет?
Cohesion измеряет сфокусированность модуля — принадлежат ли его части вместе, потому что служат единой цели и изменяются по единой причине. Спектр cohesion идёт от coincidental (произвольная группировка, самая слабая форма) через logical, temporal и communicational cohesion до functional cohesion (все части служат одной цели, все изменяются по одной причине — самая сильная форма и цель).
LCOM предоставляет структурный сигнал: класс с несколькими несвязанными кластерами методов (LCOM > 1) делает несколько вещей. Это диагностический инструмент, а не мандат — вопрос всегда в том, должны ли кластеры быть разделены на основе того, имеют ли они независимые причины для изменения.
Cohesion — противовес давлению decoupling из предыдущего урока. Минимизация попарного coupling без учёта cohesion производит чрезмерно декомпозированные модули, где координационная логика распределяется в каждый вызывающий. Правильная структурная граница — это естественная граница изменений: то, что изменяется вместе по одной причине, принадлежит вместе, и эта граница естественно производит чистый, минимальный интерфейс для внешнего мира (data coupling), потому что всё нужное внутри уже инкапсулировано.
Практика
Начни сверху. Задачи идут от простого к сложному: вспомнить факт, применить к случаю, затем senior-уровень. Открой, попробуй, потом открой ответ.
Что-то непонятно?
Задай вопрос по этому уроку. Вопросы анонимны и попадают напрямую автору — урок станет лучше.