В этой статье я бы хотел поделиться простой и практической рекомендацией о том, как программистам перестать спорить о качестве кода, а вместо этого аргументированно доказывать необходимость рефакторинга, упрощения, добавления комментариев и документации к коду. Автору никто "не заносил", но в статье я укажу на конкретный инструмент статического анализа кода, который поможет в этом, благо он бесплатный.
Цель этой статьи: рассказать не об инструменте, а о метрике, о которой пока написано и рассказано совсем немного. Уверен, что это поможет улучшить качество многих проектов и немного облегчит жизнь программистов.
Самодокументированный код
Когда мы просим программистов предоставить документацию к своему коду, или хотя бы дописать комментарии, очень часто в ответ слышится что-то вроде:
«Читай код, там всё написано»
Очень многие программисты считают свой код самодокументированным автоматически, как бы по-умолчанию.
По определению, самодокументированный код — это код, спроектированный (designed) и написанный (implemented) таким образом, что он не требует дополнительной отдельной документации. Из определения следует, что нужно применить специальные навыки проектирования (design) и потратить дополнительные усилия, чтобы этого достичь. На практике же оказывается, что самодокументированный код – это сложная задача проектирования.
Почему это так? Давайте посмотрим на то, как мы вникаем в чужой код. Сильно упростив, этот процесс можно разбить на две важные части:
Сначала нам нужно узнать, что код должен вообще делать, какова конечная задача и цель? Мы пытаем собрать некоторые знания о проекте, или части проекта, и о решаемой задачи. Как правило, для этого мы "ходим" по тикетам, опрашиваем коллег, и выясняем ЧТО нужно сделать (а особенно опытные коллеги ещё и выясняют ПОЧЕМУ).
Зная это, мы начинаем читать код и выяснять КАК именно он выполняет поставленную задачу.
Иначе говоря, читая код, мы узнаем, КАК задача решена, а не ЧТО за задача была поставлена (или ПОЧЕМУ). Узнать какую задачу код решает тоже можно, но чтобы это перестало быть предположением, вам нужно будет потратить очень много усилий (например, запустить, протестировать, и т.д.).
Некоторые утверждают, что языки вроде SQL и HTML отвечают на оба вопроса сразу. Это может быть и так, но я позволю себе это утверждение проигнорировать, так как в статье речь больше идёт о компилируемых general-purpose языках.
Программисту, читающему код, нужно "распутать ниточку", понять какую именно задачу решает код. Это принято называть "ментальной моделью решаемой проблемы". Даже если код написан откровенно плохо, быстро, без применения каких-либо специальных навыков проектирования, в нём всё равно есть какая-то задумка автора, ментальная модель. Это может быть доменная модель (если вы применяете Domain Driven Design), или же людо другой способ отражения мыслительного процесса программиста.
На хабре ранее было несколько статей о том, что такое самодокументированный код и даны конкретные рекомендации по его написанию (например, вот, вот и вот). Правильные комментарии (описывающие «ЧТО» или «ПОЧЕМУ», а не очевидное «КАК»), правильно использованные конструкции языка, чистый код – всё это также важные характеристики самодокументированного кода. Но если суммировать, то единственный способ писать самодокументированный код – это писать такой код, который в большей мере раскрывает детали модели проблемы и при этом скрывает несущественные детали имплементации.
К сожалению, зачастую происходит наоборот: детали имплементации выпячиваются наружу (например то, какая база данных применяется, протокол коммуникации, технология отображения на UI, и т.д.), не давая читателю понять, что же конкретно мы пытаемся достичь.
Ментальная модель отвечает на вопрос «ЧТО?» (то есть, какая задача, цель, почему код писали), а сам код отвечает на вопрос «КАК?».
На самом деле, ответы на вопросы «КАК?» и «ЧТО?» ортогональны, ведь одну и ту же цель можно достигнуть несколькими путями, а значит на один вопрос «ЧТО?» есть несколько ответов «КАК?». Именно тут и проявляется связь между самодокументированным кодом и навыками проектирования автора, позволяющими сделать код читаемым.
Измеряем читаемость кода
Фредерик Брукс, автор всемирно известной книги «Мифический человеко-месяц», в своём фундаментальном труде No Silver Bullet – Essence and Accident in Software Engineering) выделил два типа сложности в программном обеспечении:
Essential complexity – необходимая сложность, которая определяется самой проблемой и никак не может быть удалена.
Accidental complexity – непреднамеренная сложность, которая добавляется программистами во время проектирования и написания кода, и которая самими программистами и может быть устранена или хотя бы достаточно снижена.
Оказывается, мы уже научились измерять непреднамеренную сложность.
В 1976 году была изобретена метрика цикломатической сложности кода (Cyclomatic Complexity). К сожалению, эта метрика тесно связана со строками кода, и поэтому хорошо помогает нам в подсчёте покрытия кода тестами, но никак не помогает узнать о сложности кода. Проиллюстрирую проблему на примере:
Как видите, с точки зрения программиста, пример кода слева гораздо сложнее понять, чем код справа. Однако, величина метрики эквивалентна. Вероятно, код слева ищет сумму всех простых чисел до некоторого указанного максимального числа. Задача хорошо известна, но представьте что этот код решает не такую широкоизвестную проблему... Вам нужно будет ещё проверить, что код действительно верный:
Действительно ли название метода совпадает с поставленной задачей?
Действительно ли он решает задачу?
Какие ограничения у этого кода?
И т.д.
В 2017 году компания Sonar Source изобрела новую метрику под названием Cognitive Complexity. Как видно из примера ниже, она отлично решает поставленную задачу, явно указывая на сильно большую сложность кода слева.
О деталях реализации этой метрики отлично описано в документе, а также сам автор метрики об этом рассказывает на youtube. Если коротко, метрика основана на трех простых правилах:
Игнорировать структуры языков программирования, позволяющих сокращать написание кода и делать его более читаемым (это касается обновлённого синтаксиса любого из языков).
Увеличивать метрику на единицу для каждого оператора, прерывающего поток исполнения кода
a. Циклические конструкции for, while, do while, ...
b. Условные конструкции: if, #if, #ifdef, тернарные операторы
Увеличивать метрику на единицу в случае вложенных (nested) конструкций
Как известно, компания Sonar Source производит статические анализаторы кода, вроде SonarQube и SonarCloud, а также расширения для IDE, позволяющие более быстро получить метрики о коде. И важно то, что все эти инструменты доступны и в бесплатных версиях.
В Sonar Cloud найти эту метрику можно так: Project -> Issues -> Rules -> Cognitive Complexity
А вот как выглядит детальный отчет на командном портале SonarCloud:
В каждой строке кода указано правило, согласно которому метрика была увеличина. Это позволяет нам более гибко подходить к оценке и потенциальному рефакторингу.
К сожалению, в расширении Sonar Lint для Visual Studio 2019 эту метрику пока не успели добавить. Или просто я не умею искать :).
По-умолчанию, показателями хорошего кода являются следующие отсечки (default threshold):
Cognitive Complexity
15 (все остальные языки)
25 (семейство C-языков)
Cyclomatic Complexity = 10 (все языки)
Я добавил Cyclomatic Complexity сюда для сравнения, так как эти метрики зачастую очень сильно отличаются. Давайте взглянем на простой пример (нужно просто зайти в Sonar Cloud -> Measures -> выбрать группу Complexity):
Слева видна суммарная сложность в одной из папок с файлами кода. Тут мы уже видим разницу в 2 раза: 134 против 64. Если посмотреть на конкретные файлы, то на примере простого LoggerHelper’а видно, что мы сильно превысили когнитивную сложность кода, хотя с точки зрения цикломатической сложности - всё не так плохо. И наоборот, там, где когнитивная нагрузка равна нулю, цикломатическая сложность выше.
Выводы
Итак, мы вычислили сложность кода. Что дальше?
Теперь мы всегда сможем сказать насколько код читаемый, а самое главное - насколько он НЕчитаемый. Используя эту информацию мы можем:
Аргументированно подходить к руководству для выделения ресурсов (времени, бюджетов и т.д.) на рефакторинг, указывая на метрики и даже конкретные части проекта
Планировать рефакторинг: видеть самые плохие участки кода, с которых стоило бы начать рефакторинг в первую очередь
В случае отсутствия возможности рефакторить, увидеть участки кода, которые необходимо лучше прокомментировать и задокументировать как можно скорее
Перестать спорить с коллегами и начать избегать излишней сложности кода на ранних этапах разработки
Упростить жизнь нашим коллегам, читающим и поддерживающим наш код
В конце концов, увеличить качество продукта
К сожалению, это всё ещё не позволяет нам ответить на вопрос насколько хорошо код отражает модель решаемой проблемы. Но мне кажется это тот первый необходимый толчок, который позволит программистам начать рефакторить и приводить свой код в нужное состояние. В итоге, программисты смогут лучше отражать модели решаемых проблем в коде.
Надеюсь, я предоставил достаточно информации, чтобы заинтересовать аудиторию и начать использовать метрику Cognitive Complexity в ежедневной практике.
Всем спасибо!
adjachenko
Не очень эта ваша метрика, сами смотрите, в вашем же примере парсера я могу заменить ифы где сравнивается символ с константой на таблицу делегатов где символ будет индексом в таблице. Таким образом уйдут штрафы за часть ифов. Альтернатива иерархия наследования с виртуальными методами.
Дальше сложные условные выражения в других ифах (где за каждое подвыражение +1 штрафа) я могу разбить на составные части и результаты записать как пару результат подвыражения и как сворачивать делегат/лямбда конъюнкция/дизъюнкция в масив над которым потом сделать свертку. Вероятней всего за свертку я вообще штраф не получу ну или единицу так как там внутри будет ровно один обычный фор. Короче говоря я просто расчехлю функциональное программирование по полной программе (если вы не в курсе, то в лямбда исчислении вообще нет ифов и форов, все на лямбдах).
После такого рефакторинга ваша метрика уменьшится, но вот будете ли вы довольны?
optiklab Автор
Вы правы в том, что метрика хорошо работает для классических конструкций вроде ветвлений, и при детальной декомпозиции штрафов не будет, хотя это не всегда улучшает код.
Но представленный пример выглядит как попытка «обыграть» программу, которая лишь пытается помочь. Это один из инструментов автоматически (и быстро) показывающий потенциальные проблемы. Никто не отменял код ревью, на котором ревьювер тыкнет носом в «накрученную» логику.
Важно не забывать в чем цель. Если цель не в качественном отражении логики в коде, а в том чтобы не получить штрафов, то как по мне это довольно странно.