Семантическая и цикломатическая сложность кода
1. Семантическая сложность
Семантическая сложность — это мера того, насколько сложно понять смысл и логику кода, включая намерения разработчика и взаимодействие кода с другими частями системы. В отличие от цикломатической, эта метрика не формализована и часто субъективна, так как сильно зависит от опыта и контекста восприятия программиста.
Факторы, влияющие на семантическую сложность:
- Читаемость кода: Хорошие имена переменных и функций, комментарии, логичная структура кода помогают легче понять, что он делает.
- Уровень абстракции: Использование паттернов и уровней абстракции может либо упростить, либо усложнить понимание — зависит от того, насколько они понятны команде.
- Сложность алгоритмов: Многочисленные вложенные условия и ветвления затрудняют понимание логики.
- Зависимости и взаимодействия: Сложные связи между модулями и компонентами повышают общую сложность восприятия.
2. Цикломатическая сложность
Цикломатическая сложность измеряет количество независимых путей выполнения в программе, отражая её структурную сложность. Основана на теории графов: программа представляется как граф потока управления.
Формула расчёта цикломатической сложности M:
Цикломатическая сложность (M) для графа потока управления программы рассчитывается по формуле:
где:
- E — количество рёбер в графе.
- N — количество узлов в графе.
- P — количество связанных компонентов (для большинства программ это обычно 1).
Пример:
def example(a, b):
if a > b:
return a - b
elif a < b:
return b - a
else:
return a + b
- Узлы (N): 6 (условия и возвраты)
- Рёбра (E): 7 (ветвления условий и переходы)
- Связанные компоненты (P): 1
Вычисляем:
Значит, у программы 3 независимых пути.
Интерпретация значений цикломатической сложности:
- 1–10: Низкая, простой и понятный код
- 11–20: Средняя, возможно стоит подумать о рефакторинге
- 21–50: Высокая, сложный для поддержки и тестирования код
- 50+: Очень высокая, необходима декомпозиция на более простые части
Итог
- Семантическая сложность — субъективна, связана с восприятием и пониманием логики кода человеком.
- Цикломатическая сложность — объективна, даёт количественную оценку структуры и количества путей в коде.
Обе метрики важны для поддержания качества, удобочитаемости и тестируемости кода.
Почему тяжело понимать сложный код
Книга "Ум программиста. Как понять и осмыслить любой код." Автор - Фелин Хермане.
Когнитивная нагрузка — это предел того, что может обработать рабочая память. Рабочая память это про вычисление, работает с того момента, как мы начинаем думать о коде. Если вы испытываете большую когнитивную нагрузку, то вы не сможете правильно обработать код. Когнитивная нагрузка возникает в тот момент, когда ваша рабочая память перегружается и мозг становится не в состоянии обрабатывать поступающую информацию.
Как и кратковременная память, рабочая память способна обрабатывать от двух до шести элементов одновременно. В контексте рабочей памяти эта способность больше известна как когнитивная нагрузка. Попытка решить задачу, содержащую множество отдельных элементов, которые нельзя эффективно разделить на чанки (паттерны), приведет к «перегрузке» рабочей памяти.
Типы когнитивной нагрузки:
- Внутренняя нагрузка - Насколько задача сложна сама по себе. Задача требующая определенных знаний в этой узкой области и нет других способов решить ее или упростить. Так ее называется внтуренней сложностью.
- Внешняя нагрузка - Какие отвлекающие факторы усугубляют задачу Часто случайно, как результат отсутствия рефакторинга, требуется запоминать не нужные данные не влияющие на саму задачу, плохое оформление что мешает понять саму задачу и делает ее сложнее чем это есть на самом деле.
- Соответствующая нагрузка - Когнитивная нагрузка, созданная необходимостью хранить все мысли в долговременной памяти
Способы снижения когнитивной нагрузки - Рефакторинг. Замена незнакомых языковых конструкций.
Это рефакторинг кода до понятной и знакомой формы. Рефакторинг— это перепроектирование кода с целью улучшения его внутренней структуры, но без изменения его интерфейсов, для облегчения сопровождения кода в дальнейшем. Например убрать дублирование и разделить обьемный код на части. Однако код, который будет легок в сопровождении, не всегда может быть удобен при чтении. Поэтому иногда вам захочется сделать рефакторинг кода, чтобы упростить его чтение в данный период времени, а не для того, чтобы упростить сопровождение кода в долгосрочной перспективе. Такой рефакторинг называется когнитивным. Цель когнитивного рефакторинга заключается в том, чтобы сделать код более читаемым для данного читателя в данный момент времени. Когнитивный рефакторинг иногда может включать в себя обратный рефакторинг, который снижает удобство эксплуатации, например встраивание кода — реализация метода и копирование тела функции в место вызова. Во многих случаях когнитивный рефакторинг носит лишь временный характер и предназначен для того, чтобы человек мог понять код. Затем, как только человек начинает понимать его, когнитивный рефакторинг не используется.
Когда ваша рабочая память будет на пределе, вы можете воспользоваться специальными приемами, которые помогут вам сосредоточиться на нужных частях кода. Создание графа зависимостей для вашего кода может помочь вам понять логику и прочитать код, следуя логической последовательности.
В моем понимании, проблема нехватки рабочей памяти (сложная запутанная архитектура или сложное вычисление), например если код состоит из множества подмодулей с изменением общего состояния в них, тут без знания особенностей всех возможностей модулей не понять поведение кода, придется делать карту поведения, внешне может выглядеть как карта знания со стрелками на понятные куски кода и общую динамику timeline изменения состояния с комментариями. Процесс мысленного выполнения кода называется трассировкой или когнитивной компиляцией.
Имена переменных. Если вы не знаете, что означает та или иная переменная, то код вызовет у вас определенные сложности. Вот почему понятные имена переменных могут помочь нам углубленно понять код.
Например прикладная Венгерская нотация, в ней у префиксов есть конкретное значение, а не тип переменной (от типа в названии отказались когда появилась поддерка отображения типа в редакторах). lCustomers это length customers т.е. для понятия длины массивы пользователей, а cCoiors count colors для подсчета количества цветов.
Боритесь со сложностью ("Steve McConnell - (Code Complete)"):
- Разделите систему на подсистемы на уровне архитектуры, чтобы концентрироваться в каждый конкретный момент времени на меньшей части системы.
- Тщательно определяйте интерфейсы классов, чтобы можно было игнорировать внутреннее устройство классов. Поддерживайте абстракцию, формируемую интерфейсом класса, чтобы не запоминать ненужных деталей.
- Избегайте глобальных данных, потому что их использование значительно увеличивает процент кода, который нужно удерживать в уме в любой момент времени. Глобальные данные вносят в код неопределенность.
- Избегайте глубокой вложенности циклов и условных операторов, поскольку их можно заменить на более простые управляющие структуры, позволяющие бережнее расходовать умственные ресурсы.
- Избегайте операторов goto, так как они вносят в программу нелинейность, за которой большинству людей трудно следовать.
- Используйте ясные, очевидные имена переменных, чтобы не вспоминать детали вроде «i — это индекс счета, а j — индекс клиента или наоборот?».
- Присваивайте промежуточным переменным промежуточные результаты вычислений с целью документирования этих результатов.
Программирование развивается преимущественно за счет повышения абстрактности программных компонентов. Фред Брукс утверждает, что самым крупным достижением в компьютерных науках можно считать переход от машинного языка к высокоуровневым языкам: он освободил программистов от забот об особенностях отдельных устройств и позволил сосредоточиться на самом программировании (Brooks, 1995).
Функциональное именование переменных, отвечающее на вопрос «что?» уровня проблемы, а не «как?» уровня реализации, повышает уровень абстракции. Если вы говорите: «Я выталкиваю элемент из стека, получая данные о самом последнем сотруднике», — абстракция может избавить вас от выполнения умственного этапа «Я выталкиваю элемент из стека». Вы просто говорите: «Я получаю данные о самом последнем сотруднике». Эта выгода невелика, но если вы пытаетесь сократить диапазон сложности, простирающийся от 1 до 109, важен каждый шаг к цели.
фон GTD
Getting Things Done (GTD) — это система личной эффективности Дэвида Аллена, суть которой — выносить из головы всё, что нужно сделать, во внешнюю систему, чтобы освободить ум для концентрации на самой работе. Это достигается через пять этапов: сбор → прояснение → организация → обзор → выполнение.
Метод предназначен для уменьшения стресса и повышения продуктивности, помогает справляться с большим количеством задач и проектов.