Перейти к содержанию

Правила связности

Правила связности измеряют, насколько хорошо методы внутри класса работают вместе. Связный класс имеет методы, которые оперируют одними и теми же данными -- они разделяют свойства и преследуют единую цель. Низкая связность -- сильный сигнал того, что класс делает слишком много вещей и должен быть разделён.

Смотрите также: LCOM (Недостаток связности методов) -- дополнительная метрика связности, считающая изолированные группы методов.


LCOM -- Недостаток связности методов

Rule ID: cohesion.lcom

Судимая метрика: cohesion.lcom

LCOM4 считает изолированные группы связанных instance-методов. Значение 1 означает, что класс связный; значение больше 1 указывает на независимые группы обязанностей, которые можно разделить. Qualimetrix связывает методы, которые используют общее свойство или вызывают друг друга через $this->method(), исключает статические методы, конструктор и деструктор, и объединяет stateless constant-методы в один виртуальный узел.

Пороги warning/error по умолчанию — 3 и 5. Readonly-классы исключены по умолчанию, а в классе должно быть не менее трёх методов. Также можно исключить из графа методы, необходимые интерфейсу:

rules:
  cohesion.lcom:
    warning: 3
    error: 5
    exclude_readonly: true
    min_methods: 3
    exclude_methods: [getName, getDescription]

exclude_methods принимает и одну строку — она означает список из одного элемента: exclude_methods: getName — это ровно exclude_methods: [getName]. Строка из цифр — такое же имя метода, как любое другое.

Простой порог вместо раздельных уровней warning/error (threshold нельзя сочетать с warning или error — смешение считается ошибкой конфигурации, и прогон останавливается с кодом 3):

rules:
  cohesion.lcom:
    threshold: 3   # warning=3, error=3 → все нарушения становятся ошибками
bin/qmx check src/ --rule-opt="cohesion.lcom:threshold=3"

Особенности реализации

Qualimetrix реализует основанный на графах алгоритм LCOM4. Instance-методы становятся узлами графа, а метрика равна числу компонент связности.

Отклонение от оригинальной спецификации

Оригинальная спецификация LCOM4 Hitz и Montazeri определяет рёбра только через общий доступ к свойствам. Qualimetrix также создаёт ребро, когда один метод вызывает другой через $this->method(), поэтому хорошо декомпозированный класс с геттерами не выглядит искусственно несвязным. Stateless constant-методы без доступа к свойствам и вызовов instance-методов объединяются в один виртуальный узел; это не даёт metadata-методам, необходимым интерфейсу, например getName() и getDescription(), по отдельности увеличивать число изолированных компонент. Метод, чьё тело — единственный return класс-константы, засчитывается независимо от того, какому классу принадлежит константа: self::X, static::X, parent::X, Foo::X у импортированного класса и кейс перечисления вроде Suit::Hearts — все они не читают состояние экземпляра, поэтому все распознаются как константные выражения. Динамическое имя класса или константы ($var::X, Foo::{$name}) разрешается в рантайме и константным выражением не считается. Конструктор и деструктор (__construct, __destruct) полностью исключены из набора методов -- так же, как это уже делает TCC/LCC (см. «Особенности реализации» ниже): конструктор, чьи присвоенные поля не читает ни один другой stateful-метод, не делит с остальным классом ни одного ребра доступа к свойству, поэтому без исключения он оставался бы в графе изолированной вершиной и увеличивал LCOM на единицу. Property promotion (private array $x в списке параметров) -- гарантированный случай: promoted-параметр вообще никогда не порождает узел доступа к свойству -- это касается подавляющего большинства конструкторов в современном PHP 8+. Вариант учитывать promoted-параметры как доступ к свойствам был рассмотрен и отклонён: он превратил бы __construct в хаб, касающийся каждого promoted-свойства, связывающий несвязанные методы через себя и почти везде понижающий LCOM до 1, что уничтожило бы сигнал метрики, а не исправило его. Исключение действует не в одну сторону: откат на исходном коде php-parser (там promoted-конструкторов нет вовсе) изменил LCOM у 106 из 260 классов -- у 97 он упал (снят как раз этот артефакт), а у 9 вырос (конструктор маскировал настоящую несвязность), а health.cohesion сдвинулся с 60.40 до 63.13.

Сравнение с другими инструментами

phpmetrics использует формулу Henderson-Sellers LCOM с другой шкалой значений. Его значения нельзя напрямую сравнивать со значениями LCOM4 Qualimetrix.


TCC -- Тесная связность класса

Metric ID: cohesion.tcc

Что измеряет

TCC измеряет, насколько связаны публичные методы класса через общий доступ к свойствам. Если два публичных метода оба читают или пишут одно и то же свойство ($this->property), они считаются напрямую связанными.

TCC = NDC / NP

Где:

  • NDC = количество напрямую связанных пар методов (пары публичных методов, разделяющих хотя бы одно свойство)
  • NP = максимально возможное количество пар = N x (N - 1) / 2
  • N = количество отслеживаемых публичных методов

Результат -- отношение от 0.0 до 1.0:

  • TCC = 1.0 -- каждый публичный метод разделяет свойства с каждым другим. Класс идеально связный.
  • TCC >= 0.5 -- хорошая связность. Большинство методов работают с одними и теми же данными.
  • TCC < 0.3 -- низкая связность. Методы работают с разными подмножествами свойств -- класс, вероятно, имеет несколько ответственностей.

Представьте званый ужин: если каждый гость знает каждого другого гостя, группа тесно связана (TCC = 1.0). Если гости формируют изолированные клики без пересечений, лучше было бы провести два отдельных мероприятия (TCC близко к 0.0).

Как читать значение:

TCC Интерпретация
0.5--1.0 Хорошо -- методы хорошо связаны между собой
0.3--0.5 Умеренная связность
Ниже 0.3 Низкая связность -- рассмотрите разделение

Пороговые значения

TCC и LCC в настоящее время доступны как метрики (видимы в выводе --format=metrics). Они не генерируют нарушения самостоятельно. Используйте их вместе с LCOM для полной картины связности класса.

Рекомендуемая интерпретация:

Значение TCC Значение
1.0 Идеальная связность -- все публичные методы разделяют свойства
>= 0.5 Хорошая связность
0.3--0.5 Умеренная -- проверьте, не слишком ли много обязанностей у класса
< 0.3 Низкая связность -- класс, вероятно, нужно разделить

Пример

class OrderService
{
    private array $items = [];
    private float $total = 0.0;
    private string $customerEmail;
    private string $customerName;

    // Группа 1: работает с $items и $total
    public function addItem(string $item, float $price): void  // -> $this->items, $this->total
    {
        $this->items[] = $item;
        $this->total += $price;
    }

    public function getTotal(): float  // -> $this->total
    {
        return $this->total;
    }

    public function getItems(): array  // -> $this->items
    {
        return $this->items;
    }

    // Группа 2: работает с $customerEmail и $customerName
    public function setCustomer(string $name, string $email): void  // -> $this->customerName, $this->customerEmail
    {
        $this->customerName = $name;
        $this->customerEmail = $email;
    }

    public function getCustomerEmail(): string  // -> $this->customerEmail
    {
        return $this->customerEmail;
    }
}

С 5 публичными методами NP = 5 x 4 / 2 = 10 возможных пар. Только несколько пар разделяют свойства (например, addItem-getTotal разделяют $total, addItem-getItems разделяют $items, setCustomer-getCustomerEmail разделяют $customerEmail). Две группы не пересекаются, поэтому TCC будет низким (около 0.3).

Как исправить

  • Разделите класс по границам свойств. В примере: OrderCart для items/total и CustomerInfo для name/email.
  • Посмотрите, какие свойства группируются вместе. Методы, разделяющие свойства, принадлежат вместе; методы, которые не разделяют -- должны быть в отдельных классах.
  • Используйте значение TCC вместе с LCOM. LCOM считает изолированные группы; TCC показывает, какая доля пар методов связана. Вместе они дают полную картину связности.

LCC -- Свободная связность класса

Metric ID: cohesion.lcc

Что измеряет

LCC расширяет TCC, включая транзитивные связи. Два метода считаются свободно связанными, если они соединены через цепочку напрямую связанных методов, даже если сами они не разделяют ни одного свойства.

LCC = NIC / NP

Где:

  • NIC = количество косвенно связанных пар (все пары, достижимые через прямые связи)
  • NP = максимально возможное количество пар

LCC всегда >= TCC для одного и того же класса. Если TCC = LCC, транзитивных связей нет. Если LCC значительно выше TCC, класс имеет «цепочечную» структуру, где методы связаны через посредников.

Соотношение Значение
TCC = LCC Все связи прямые -- нет транзитивных цепочек
LCC >> TCC Методы образуют цепочки -- проверьте, намеренна ли такая архитектура

Пример

Рассмотрим три метода A, B и C:

  • A и B оба обращаются к $this->data (напрямую связаны)
  • B и C оба обращаются к $this->cache (напрямую связаны)
  • A и C не разделяют свойств (не связаны напрямую)

TCC считает 2 прямые пары из 3 возможных: TCC = 2/3 = 0.67. LCC считает все 3 пары (A-C транзитивно связаны через B): LCC = 3/3 = 1.0.


Особенности реализации

Qualimetrix реализует упрощённый вариант спецификации TCC/LCC Bieman & Kang (1995):

  • Конструкторы и деструкторы (__construct, __destruct) исключены из набора методов по спецификации B&K (это методы настройки/очистки, а не поведенческие).
  • Перечисления (enums) исключены -- они не могут иметь свойств экземпляра, поэтому TCC всегда был бы 0.0, что вводит в заблуждение.
  • Интерфейсы исключены -- у них нет тел методов, поэтому доступ к свойствам невозможно измерить.
  • Учитываются только публичные методы. Оригинальная статья B&K указывает «видимые методы» (public + protected); Qualimetrix следует более строгому отраслевому соглашению (только public), что согласуется с большинством инструментов (PHPMD, PHPMetrics).
  • Учитывается только прямой доступ через $this->property. B&K также определяет «деревья вызовов», где публичный метод, вызывающий приватный вспомогательный метод, обращающийся к свойству, считается косвенным доступом. Это не реализовано -- делегирование через приватные методы не отслеживается. Это означает, что TCC может быть занижен для классов, активно использующих паттерн делегирования.
  • Статические и абстрактные методы исключены -- они не оперируют состоянием экземпляра.
  • Классы с 0 или 1 отслеживаемым публичным методом по умолчанию имеют TCC = 1.0 и LCC = 1.0 (класс с одним методом тривиально связный).

Сравнение с другими инструментами

Большинство инструментов (PHPMD, PHPMetrics, JArchitect) реализуют тот же упрощённый вариант -- только прямой доступ к свойствам, без деревьев вызовов. Значения должны быть сопоставимы между инструментами, хотя могут быть незначительные различия в обработке конструкторов или статических методов.


Настройка

TCC и LCC собираются как метрики и не имеют настраиваемых пороговых значений. Они отображаются в выводе metrics JSON:

bin/qmx check src/ --format=metrics

Для использования TCC/LCC в качестве контрольных показателей можно обрабатывать вывод metrics JSON программно (например, в скрипте CI-пайплайна).