4.1 Метрики сложности кода
Обзор и мотивация
Цикломатическая сложность, введённая Томасом Дж. МакКейбом в 1976 году, считает число независимых путей через поток управления кода: каждый if, цикл и ветвление добавляется к подсчёту. Она остаётся самой широко используемой метрикой сложности кода почти пятьдесят лет спустя, наряду с родственниками вроде когнитивной сложности (которая взвешивает вложенный и труднопрослеживаемый поток управления сильнее, чем оригинальный линейный подсчёт МакКейба) и глубины вложенности. Эти метрики разделяют подлинное подтверждённое озарение: код с большим числом независимых путей через него труднее полностью протестировать, труднее о нём рассуждать и, по десятилетиям эмпирических исследований, измеримо с большей вероятностью содержит дефекты.
Эта тема относится к этому озарению с настоящим уважением, относясь при этом к его ограничениям с равной серьёзностью. Метрики сложности измеряют одно конкретное свойство кода, и кодовая база может быть простой по каждой метрике сложности, оставаясь при этом плохо спроектированной, плохо названной или концептуально несвязной способами, которые никакой алгоритм подсчёта ветвлений не может обнаружить. И наоборот, некоторые несводимо сложные проблемы подлинно требуют сложного кода для правильного решения, и команда, испытывающая давление минимизировать оценку сложности, может произвести код, хорошо оцениваемый, оставаясь при этом фактически труднее для понимания, распределяя существенную сложность по большему числу файлов и слоёв косвенности, а не сокращая её.
Для крупных команд метрики сложности оправдывают себя как инструмент триажа: способ найти среди тысяч файлов небольшое подмножество, наиболее вероятно заслуживающее более пристального взгляда, а не как самостоятельный приговор о качестве кода. Корпоративные и государственные организации, поддерживающие кодовые базы, слишком большие для того, чтобы кто-либо прочитал их полностью, полагаются на эту функцию триажа, чтобы направить дефицитные усилия рефакторинга и ревью туда, где они принесут больше всего пользы.
Ключевые принципы
- Метрики сложности предсказывают трудность тестирования и дефектов; они не измеряют качество напрямую. Относитесь к ним как к одному входу, а не приговору.
- Оценка сложности подвержена манипулированию через запутывание, а не только подлинное упрощение. Разделение сложности по большему числу файлов может снизить оценку, фактически не делая код легче для понимания.
- Некоторая сложность существенна, а не случайна. Подлинно сложная проблема может требовать подлинно сложного кода; цель минимизация случайной сложности, а не устранение всей сложности без разбора.
- Используйте метрики сложности для триажа, а не как индивидуальную или командную оценочную карточку. Они указывают, куда смотреть, а не кого винить.
- Тренд и выбросы важнее любого абсолютного порога. Растущий тренд или экстремальный выброс более действенны, чем единственное общекомандное среднее.
Рекомендации
Используйте метрики сложности для триажа усилий ревью и рефакторинга
Проведите анализ сложности по кодовой базе и используйте результаты для приоритизации того, где более пристальное человеческое ревью или инвестиция в рефакторинг окупятся больше всего: функции или файлы, оцениваемые намного выше типичного диапазона самой кодовой базы, это места наивысшей ценности для рассмотрения в первую очередь. Это использование для триажа, нахождение, куда смотреть, самое защитимое и ценное применение метрик сложности, гораздо более так, чем использование их как абсолютных ворот прохода/отказа.
Устанавливайте пороги относительно вашей собственной кодовой базы, а не универсального числа
Абсолютные пороги сложности, некритично заимствованные из отраслевой конвенции (оценка сложности десять часто цитируемое эмпирическое правило), могут быть либо слишком снисходительными, либо слишком строгими в зависимости от вашего домена: у парсера или движка правил может быть законно более высокая базовая сложность, чем у типичного CRUD-сервиса. Калибруйте ваши собственные пороги против фактического распределения вашей кодовой базы и относитесь к нарушению порога как к побуждению посмотреть ближе, а не автоматическому сбою сборки, если только ваша команда сознательно не выбрала эту более строгую политику с полным осознанием её компромиссов.
Следите за манипулированием через декомпозицию без подлинного упрощения
Самый распространённый способ манипулирования оценками сложности это паттерн подмены из темы 1.2, применённый к этой конкретной метрике: разделение одной подлинно сложной функции на несколько меньших функций, индивидуально хорошо оцениваемых, в то время как общая система остаётся такой же трудной для понимания, или иногда становится труднее, потому что логика теперь рассеяна по большему числу файлов с большей косвенностью между ними. Сочетайте метрики сложности с качественным ревью того, действительно ли декомпозиция подлинно прояснила код, или она просто переместила сложность туда, где метрика больше не могла её видеть.
Отличайте существенную сложность от случайной сложности, прежде чем реагировать
Прежде чем относиться к высокой оценке сложности как к проблеме, которую нужно исправить, спросите, действительно ли лежащая в основе проблема требует столько независимых путей, например, логика расчёта налогового кодекса законно имеет много ветвлений, или сложность происходит от избегаемых причин: глубоко вложенных условий, которые можно сгладить, дублированной логики, которую можно консолидировать, или неясных границ ответственности, которые можно перерисовать. Только вторая категория подлинная проблема качества, которую эта метрика должна побудить вас исправить.
Отслеживайте тренд и выбросы, а не только средний снимок
Среднюю оценку сложности по всей кодовой базе, немного двигающуюся, редко можно использовать саму по себе; сложность конкретного файла, резко растущая за несколько изменений, или небольшое число экстремальных выбросов в иначе хорошо ведущей себя кодовой базе, гораздо более полезные сигналы. Отслеживайте и тренд во времени, и хвост выбросов, и используйте их для запуска конкретного целевого исследования, а не широкой нефокусированной инициативы по снижению сложности.
Компромиссы: плюсы и минусы
| Подход | Плюсы | Минусы |
|---|---|---|
| Абсолютный универсальный порог | Просто, последовательно, легко автоматизировать | Игнорирует законные различия доменов; можно подделать декомпозицией |
| Порог, относительный к кодовой базе | Лучше откалиброван к фактическому контексту | Требует больше настройки и периодической перекалибровки |
| Сложность как автоматизированные ворота сборки | Обеспечивает согласованность без накладных расходов человеческого ревью | Может блокировать законно сложный, но хорошо спроектированный код, или вознаграждать запутанную декомпозицию |
| Сложность как сигнал триажа для человеческого ревью | Улавливает подлинные проблемы качества, которые одна декомпозиция упустила бы | Требует больше времени человеческого ревью, чем полностью автоматизированные ворота |
Центральное напряжение: автоматизация против суждения. Полностью автоматизированные ворота сложности дёшевы в применении и последовательны, но они могут и блокировать законно сложный хорошо спроектированный код, и вознаграждать поверхностную декомпозицию, подделывающую оценку, не упрощая на самом деле ничего. Разрешайте это напряжение, используя автоматизированный анализ сложности, чтобы раскрыть кандидатов для ревью, и резервируя фактическое суждение, существенна ли эта сложность или случайна, подлинно ли этот рефакторинг прояснил или просто переместил сложность, для человеческого рецензента, а не одних жёстких автоматизированных ворот.
Вопросы для обсуждения в команде
Откалиброваны ли наши пороги сложности к фактическому распределению нашей собственной кодовой базы, или некритично заимствованы из общей отраслевой конвенции? Возьмите реальное распределение сложности вашей кодовой базы и проверьте, имеют ли смысл ваши текущие пороги против него, а не предполагайте, что часто цитируемое число применяется универсально к вашему домену.
Видели ли мы когда-либо, как функция разделялась на несколько меньших без того, чтобы результирующий код на самом деле становился легче для понимания? Это самый ясный признак паттерна манипулирования декомпозицией, против которого предупреждает эта тема. Посмотрите на недавний рефакторинг, мотивированный в первую очередь оценкой сложности, и честно оцените, улучшил ли он подлинную понятность.
Где в нашей кодовой базе сложность существенна для проблемы, а где случайна и исправима? Пройдите через ваши выбросы наивысшей сложности и явно рассортируйте их на эти две категории, поскольку только вторая категория представляет подлинную действенную проблему качества.
Используем ли мы метрики сложности для триажа усилий ревью, или как жёсткие автоматизированные ворота без вовлечённого человеческого суждения? Обсудите, оставляет ли ваш текущий подход применения место для различия существенного против случайного, которое рекомендует эта тема, или он относится к каждому нарушению одинаково независимо от контекста.
Использовалась ли оценка сложности когда-либо, даже неформально, для суждения о качестве работы отдельного инженера? Это рискует той же ловушкой индивидуальной оценки, против которой предупреждает тема 3.4 для метрик активности, применённой здесь к метрикам кода вместо этого, и это приглашает тот же ответ манипулирования.
Как выглядит наш тренд сложности за прошлый год для наших самых критических самых часто меняемых файлов? Сочетайте это с анализом объёма изменений и горячих точек из темы 4.3, поскольку файл, одновременно высоко сложный и часто меняемый, заслуживает внимания гораздо раньше, чем тот, что сложен, но редко трогается.
Отраслевой взгляд
Стартап. Метрики сложности обычно менее срочны в этом масштабе; размер кодовой базы достаточно мал, что неформальная знакомость часто заменяет формальное измерение. Привычка, которую стоит принять рано, это просто периодически проводить сканирование сложности, чтобы поймать конкретный файл, тихо становящийся неуправляемым, прежде чем команда вырастет слишком большой, чтобы заметить это неформально.
Малый бизнес. Большинство современных инструментов статического анализа сообщают метрики сложности как часть более широкой бесплатной или недорогой настройки линтинга; используйте вывод как периодический сигнал триажа, а не инвестируйте в выделенный инструментарий. Сосредоточьте внимание на ваших самых часто изменяемых файлах в первую очередь.
Корпорация. Метрики сложности в масштабе наиболее ценны в сочетании с данными объёма изменений (тема 4.3) для приоритизации инвестиций в рефакторинг по кодовой базе, слишком большой для ручного обзора любым отдельным человеком. Калибруйте пороги по сервису или домену, а не применяйте одно общеорганизационное число, поскольку законная сложность значительно варьируется по разным видам систем.
Государство. Долгоживущие государственные системы часто накапливают сложность постепенно за годы или десятилетия инкрементальных изменений требований, и аудит сложности может быть убедительным конкретным инструментом для обоснования инвестиции в модернизацию или рефакторинг перед заинтересованными сторонами, которые иначе могли бы видеть систему просто как «работающую» и поэтому не стоящую инвестиций.
Примеры
Корпорация. Компания обработки платежей впервые провела аудит сложности по всей кодовой базе и нашла единственную функцию валидации транзакций с оценкой цикломатической сложности более чем в десять раз выше медианы кодовой базы. Исследование обнаружило, что сложность была почти полностью случайной: годы инкрементально добавленной обработки особых случаев для конкретных платёжных провайдеров накопились в глубоко вложенные условия, которые можно было реструктурировать в более чистый паттерн стратегии, отделяющий логику, специфичную для провайдера. Рефакторинг, приоритизированный напрямую, потому что аудит сложности определил его как единственную цель наивысшей ценности в кодовой базе, снизил оценку сложности функции более чем на 80% и, что более важно, измеримо снизил частоту дефектов в этом конкретном пути кода за следующие два квартала.
Государство. Десятилетиями старый движок расчёта пособий налогового органа оценивался чрезвычайно высоко по метрикам сложности почти по каждой функции, побуждая первоначальное предположение, что вся система нуждается в полной переписке с нуля. Более пристальный пофункциональный обзор, отличающий существенную сложность от случайной, обнаружил, что большая часть сложности подлинно отражала лежащие в основе юридические правила, у которых действительно было столько законных ветвлений и особых случаев, предписанных статутом, в то время как меньшее подмножество происходило от избегаемого дублирования по схожим путям вычисления. Команда нацелилась только на подмножество случайной сложности для рефакторинга, избегая дорогостоящей рискованной полной переписки, при этом значимо улучшая действительно проблемные области системы.
Бизнес-кейс: мотивация, ROI и TCO
Отдача от хорошего использования метрик сложности: целенаправленная высокоценная инвестиция в рефакторинг: пример компании обработки платежей выше показывает единственное хорошо нацеленное исправление, определённое через анализ сложности, измеримо снизившее дефекты именно в самом высокорисковом пути кода, за малую долю стоимости, которую потребовала бы широкая нецеленаправленная инициатива рефакторинга.
Полная стоимость владения низкая: большинство современных инструментальных цепочек разработки вычисляют метрики сложности автоматически как часть статического анализа (тема 4.4), а реальная инвестиция это время человеческого суждения для правильной интерпретации результатов, отличения существенной сложности от случайной и поимки манипулирования декомпозицией, а не какая-либо значительная новая стоимость инструментария.
Антипаттерны и ловушки
- Отношение к оценке сложности как к прямому приговору качества: она измеряет одно конкретное свойство, а не общее качество кода.
- Разделение функции для подделки оценки без подлинного упрощения: паттерн манипулирования декомпозицией, который конкретно называет эта тема.
- Применение универсального порога без калибровки к вашей собственной кодовой базе: производит либо слишком снисходительное, либо слишком строгое применение в зависимости от домена.
- Использование метрик сложности для индивидуальной оценки инженеров: приглашает манипулирование и неправильно применяет метрику, предназначенную для триажа, а не суждения.
- Отношение ко всей сложности как к одинаково исправимой: существенная сложность от подлинно сложной проблемы не дефект, который нужно устранять.
- Игнорирование тренда и выбросов в пользу плоского среднего по кодовой базе: упускает самый действенный сигнал, который предоставляет это семейство метрик.
Модель зрелости
- Уровень 1, Инициация: Сложность не измеряется, или измеряется с неисследованным общим универсальным порогом, применяемым некритично.
- Уровень 2, Развитие: Метрики сложности собираются, но редко приводят к действию, и не делается различия между существенной и случайной сложностью.
- Уровень 3, Стандартизация: Пороги откалиброваны к собственному распределению кодовой базы, а метрики сложности последовательно направляют триаж ревью и рефакторинга по всей организации.
- Уровень 4, Управление: Тренд сложности и выбросы активно отслеживаются и сочетаются с данными объёма изменений (тема 4.3) для приоритизации инвестиций в рефакторинг; за манипулированием декомпозицией активно следят.
- Уровень 5, Оркестрация: Организация может указать на конкретные измеримые улучшения частоты дефектов, напрямую прослеженные до информированной сложностью инвестиции в рефакторинг, а данные сложности это рутинный доверенный вход в решения об инженерных инвестициях.
Идеи для обсуждения
- Какая наша единственная самая сложная функция или файл, и существенна ли её сложность или случайна?
- Подделывали ли мы когда-либо оценку сложности через декомпозицию без реального упрощения?
- Откалиброваны ли наши пороги к нашей собственной кодовой базе, или заимствованы некритично?
- Где высокая сложность пересекается с высоким объёмом изменений в нашей кодовой базе прямо сейчас?
- Информировали ли когда-либо данные сложности решение об инвестиции в рефакторинг, или они лежат неиспользованными?
Основные выводы
- Метрики сложности вроде цикломатической сложности предсказывают трудность тестирования и дефектов; они не измеряют общее качество кода напрямую.
- Отличайте существенную сложность (от подлинно сложной проблемы) от случайной сложности (избегаемой через лучший дизайн), прежде чем реагировать на высокую оценку.
- Следите за манипулированием декомпозицией: разделением кода для снижения оценки без подлинного упрощения чего-либо.
- Используйте метрики сложности для триажа, направляя усилия человеческого ревью и рефакторинга, а не как индивидуальную оценочную карточку или жёсткие автоматизированные ворота.
- Калибруйте пороги к распределению вашей собственной кодовой базы и отслеживайте тренд и выбросы, а не только плоское среднее.
Источники и дальнейшее чтение
- McCabe, Thomas J., “A Complexity Measure,” IEEE Transactions on Software Engineering (1976).
- Code Complete, Стив МакКоннелл.
- Working Effectively with Legacy Code, Майкл Физерс.
- Campbell, G. Ann, “Cognitive Complexity: A New Way of Measuring Understandability” (SonarSource, 2018).