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

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

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

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


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

Rule ID: cohesion.lcom

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

LCOM4 считает изолированные группы связанных instance-методов. Значение 1 означает, что класс связный; значение больше 1 указывает на независимые группы обязанностей, которые можно разделить. Qualimetrix связывает методы, которые используют общее свойство или вызывают друг друга через $this->method(), self::method() или static::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 требует YAML sequence строк. Пиши exclude_methods: [getName]; scalar string или CSV отказывают. Элементы сохраняют авторское написание и сопоставляются с method names без учёта регистра.

Простой порог вместо раздельных уровней 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-методы становятся узлами графа, а метрика равна числу компонент связности. Пустой граф не имеет значения LCOM. cohesion.lcom-method-count показывает подходящую популяцию методов, а cohesion.unread-trait-count — число неразрешённых использований трейтов. Методы разрешённых трейтов участвуют после алиасов, изменений видимости, insteadof и переопределений класса; неразрешённый трейт не считается пустым.

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

Hitz и Montazeri определяют рёбра через общие переменные экземпляра или вызовы методов. Qualimetrix следует этим двум видам рёбер, учитывая регистронезависимые имена методов PHP и эффективные методы трейтов. Адаптация области касается популяции методов и пустого графа, у которого нет значения. Stateless constant-методы без доступа к свойствам и вызовов instance-методов объединяются в один виртуальный узел, только если их никто не вызывает; это не даёт metadata-методам, необходимым интерфейсу, например getName() и getDescription(), по отдельности увеличивать число изолированных компонент. Метод, чьё тело — единственный return класс-константы, засчитывается независимо от того, какому классу принадлежит константа: self::X, static::X, parent::X, Foo::X у импортированного класса и кейс перечисления вроде Suit::Hearts — все они не читают состояние экземпляра, поэтому все распознаются как константные выражения. Динамическое имя класса или константы ($var::X, Foo::{$name}) разрешается в рантайме и константным выражением не считается. Конструктор и деструктор (__construct, __destruct) исключены из популяции LCOM как адаптация области. TCC/LCC также исключают их, как определяют Bieman и Kang. Инициализация не является поведенческой группой; рёбра конструктора не должны соединять независимые обязанности.

Приватные instance-методы, которые достигает только конструктор или деструктор через вызовы $this->, self:: или static:: (напрямую или через другие такие хелперы), — хелперы инициализации, и они тоже исключаются. Иначе каждый образовал бы собственную изолированную компоненту, потому что ребро конструктора, соединяющее его с классом, удалено. Метод остаётся в графе, если его достигает любой другой метод: public- и protected-методы никогда не считаются хелперами, даже если их вызывает конструктор, как и static-методы и элементы exclude_methods, а их вызовы обычные. Замыкание, стрелочная функция или first-class callable, созданные в конструкторе или хелпере, выполняются после построения, поэтому названный ими метод остаётся в графе; так считается любое замыкание, даже вызванное сразу (array_map), — его callee остаются в графе. Хелпер, передающий состояние дальше, регистрирует поведение и тоже остаётся: он трогает свойство внутри замыкания, стрелочной или вложенной функции (по имени или динамически), связывает свойство, его элемент или член по ссылке (по любую сторону =&, как элемент массива, как значение foreach или деструктуризацией) либо является генератором (yield, yield from). Удаление хелпера может повысить LCOM, если он был единственным мостом между двумя группами свойств, и может опустошить граф — тогда значение не публикуется. TCC/LCC не меняются.

Прочие ссылки на собственный метод только защищают его цель от этого удаления; рёбрами LCOM4 или TCC/LCC они не становятся. Это: вызов через собственное полное имя класса или $this::, литеральное имя в фигурных скобках ($this->{'m'}(), self::{'m'}()), двухэлементный массив или new ReflectionMethod(X, 'm'), где X — $this, self::class, static::class, собственный ::class, $this::class, __CLASS__, get_class($this), get_class(), get_called_class() или строка 'self', 'static' либо собственное полное имя, а также callable-строки 'self::m', 'static::m' и 'App\Own::m'. Сопоставление намеренно широкое: ложная ссылка лишь оставляет хелпер в графе.

Ни один метод не покидает граф, если карта вызовов может быть неполной: когда используемый трейт не прочитан (cohesion.unread-trait-count больше нуля), когда в собственном вызове или в любой ссылке выше есть нелитеральное имя ($this->$name(), self::$name(), $this::{$x}(), [$this, $name]), когда строка или строка с интерполяцией начинается с собственного префикса и ::, не будучи простой callable-строкой, или когда конкатенация начинается с собственного имени класса. Не учитываются, и хелпер, используемый только так, считается инициализацией: вызовы на другом экземпляре ($other->m(), в том числе алиас $self = $this), nullsafe $this?->m(), parent::m(), короткое имя класса, callable-строка, собранная через sprintf() или из переменной с именем класса, reflection-поиск вроде getMethod('m'), передача свойства по ссылке как аргумента и объект из свойства, захваченный по значению в статическом замыкании. call_user_func($this) достигает только публичного __invoke — обычного корня.

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

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). В разделах 2–3 учитывается состояние, достигнутое через прямо или транзитивно вызываемые методы того же объекта:

  • Конструкторы и деструкторы (__construct, __destruct) исключены из набора методов по спецификации B&K (это методы настройки/очистки, а не поведенческие).
  • Перечисления (enums) исключены -- они не могут иметь свойств экземпляра, поэтому TCC всегда был бы 0.0, что вводит в заблуждение.
  • Интерфейсы исключены -- у них нет тел методов, поэтому доступ к свойствам невозможно измерить.
  • Учитываются только публичные методы. Оригинальная статья B&K указывает «видимые методы»; Qualimetrix измеряет публичный интерфейс и учитывает приватных помощников косвенно через публичные вызывающие методы.
  • Деревья вызовов учитываются. Доступ к свойству через вызов instance-метода, в том числе приватного помощника, входит в набор свойств публичного вызывающего метода. TCC считает пары с общими свойствами в этих наборах; LCC дополнительно считает цепочки таких пар.
  • Статические и абстрактные методы исключены -- они не оперируют состоянием экземпляра.
  • Классы с 0 или 1 отслеживаемым публичным методом вовсе не получают значения TCC и LCC: при менее чем двух методах нет ни одной пары, и отношение не определено (0/0). Метрики в выводе класса отсутствуют, а не равны 1.0.
  • Агрегаты уровня пространства имён и проекта (cohesion.tcc.avg, cohesion.tcc.min и аналоги для LCC) вычисляются только по классам, у которых значение есть; .count показывает, по скольким. Health cohesion использует только доступные вклады TCC и LCOM, без подстановки TCC 0.5 или 0; классы без подходящих методов не имеют измеренного LCOM (см. Оценки здоровья).

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

Классы без применимого использования свойств экземпляра тоже не получают значения TCC и LCC, даже при двух и более отслеживаемых методах. По формуле Bieman & Kang у такого класса есть пары методов, но нет общих переменных экземпляра, так что TCC = LCC = 0. Qualimetrix считает связность через общее состояние неприменимой к классу без состояния, а не сообщает его максимально несвязным, — иначе каждый сервис без состояния тянул бы вниз средние значения пространства имён.

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

Инструменты, считающие только прямой доступ к свойствам, могут давать другие значения TCC/LCC для классов с делегированием. Перед сравнением проверь популяцию методов и учёт вызовов.


Настройка

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

bin/qmx check src/ --format=metrics

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

Ненайденные configured method names

exclude_methods — sequence с PHP case-insensitive method matching и сохранением первого авторского написания. Когда измеренный охват допускает утверждения об отсутствии деклараций LCOM producer также публикует cohesion.unmatched-exclude-method один раз на unmatched normalized name. Functions и property hooks не являются Method-kind свидетельством; partial run не доказывает отсутствие во всём проекте. Secondary warning несёт project magnitude 1 и semantic occurrence нормализованного имени, без configured warning boundary. Primary LCOM band 3/5 и lifecycle secondary channel не меняются. CLI: --lcom-exclude-methods='[getName, getDescription]'. Сообщение заканчивается указанием конфигурации, записавшей список (Written by option --lcom-exclude-methods (written as --lcom-exclude-methods=[getName]). для CLI); список из общей базы судится, потому что exclude_methods, записанный в вашем файле, целиком его заменяет.

Сравнение с baseline

Семь composed метрик класса (LCOM, TCC, LCC, две популяции методов, число чистых методов и число непрочитанных трейтов) объявляют Run reach. Тело трейта в другом файле может изменить метрики выбранного класса. Поэтому проверка только файла класса против baseline всего проекта оставляет эти потолки несравнёнными, даже если у конкретного класса все трейты прочитаны. Для сравнения нужно совпадающее полное покрытие запуска. Пофайловые сырые facts сохраняют Members reach; точные declaration subjects сохраняются в обеих фазах.