
Ваш код слишком сложен?
11 марта 2022 г.Фото Аарона Бердена на Unsplash
Мы все были там.
Взглянем на репозиторий, из которого мы не можем понять голову или решку. Или смотрим на функцию, читаем ее, доходим до конца, а затем понимаем, что нам нужно прочитать ее еще пять или шесть раз, прежде чем мы сможем понять, что она делает.
Существует несколько различных способов измерить качество кода в этой области и оценить, легко ли он понятен, но сегодня давайте сосредоточимся, в частности, на двух показателях сложности — цикломатической сложности и ее младшем брате, когнитивной сложности.
Что такое цикломатическая сложность?
Идея цикломатической сложности была впервые предложена в 1976 году Томасом МакКейбом как способ выяснить, какое программное обеспечение будет сложно тестировать или поддерживать.
Cyclomatic Complexity может дать хорошее представление о том, насколько сложно понять код (подробнее об этом чуть позже), а также может помочь вам определить минимальное количество тестов, необходимых для полного покрытия тестами той части кода, которую вы выполняете. смотря на.
Чтобы измерить цикломатическую сложность, вы можете посмотреть на граф потока управления для раздела кода, чтобы определить количество независимых путей для этого исходного кода.
Просматривая блок-схемы, чтобы понять сложность
Граф потока — это карта того, что происходит в исходном коде, путем связывания путей потока между различными задачами обработки, состоящими из узлов (задач обработки) и ребер (путей, соединяющих задачи обработки).
Вот несколько примеров блок-схем для некоторых простых функций:
```питон
защита is_even (число):
если число % 2 == 0:
number_type = "Четный"
еще:
number_type = "Нечетный"
печать (тип_числа)
```питон
определение объединения_списков():
список_1 = [1, "а"]
список_2 = [1, 2, 3]
list_3 = ["с", "д"]
list_of_lists = [список_1, список_2, список_3]
объединенные_списки = []
для i в len(list_of_lists):
join_lists.append(list_of_lists[i])
печать (соединенные_списки)
Когда у вас есть блок-схема, вы можете быстро рассчитать сложность любого фрагмента кода:
V(G) = E - N + 2
Где E — количество ребер, N — количество узлов, а V(G) — общая сложность. Вы также можете рассчитать его по:
V(G) = P + 1
Где P — количество предикатных узлов (узлов, содержащих условия).
Давайте посмотрим, насколько сложными были те функции, которые мы наметили ранее.
В примере 1 у нас есть 6 узлов и 6 ребер, поэтому наша общая цикломатическая сложность равна 2 (V (G) = 6 - 6 + 2)
В примере 2 у нас снова 6 узлов и 6 ребер, поэтому общая сложность равна 2. И оператор if в примере 1, и цикл for в примере 2 увеличивают эту сложность.
Что означают эти цифры?
В общем, для любой метрики сложности программного обеспечения чем меньшее число, тем лучше, и не существует верхнего предела того, какой может быть цикломатическая сложность части программного обеспечения. Но что такое «хорошая» оценка когнитивной сложности? И насколько высокая оценка сложности слишком высока? К счастью, NIST собрал некоторые относительно простые рекомендации, которые распределяют уровни сложности на основе оценки цикломатической сложности:
< 10 – простая программа с небольшим риском 11–20 – более сложные программы с умеренным риском 21–50 – высокая сложность с высоким риском > 50 – программа, не подлежащая тестированию с очень высоким риском.
Рейтинги NIST много говорят о тестируемости — больше, чем о сложности или понятности.
Это связано с тем, что количество тестов, необходимых для полного покрытия тестами, эквивалентно цикломатической сложности программы. Таким образом, сложные программы становится почти невозможно полностью протестировать.
Каковы недостатки цикломатической сложности? И есть ли лучший вариант?
Цикломатическая сложность может быть полезна для определения тестируемости и ремонтопригодности кода, но это не всегда лучший инструмент для определения того, насколько код понятен разработчику-человеку. На это есть три основные причины:
- При расчете цикломатической сложности каждый метод имеет минимальную сложность 1. Мы можем легко получить два класса с одинаково высокими показателями сложности, где один из них является большим, но простым в обслуживании классом с рядом простых методов, а другой представляет собой чрезвычайно сложный, но более короткий класс. Выше уровня класса оценка сложности тесно связана с количеством строк кода, поэтому более длинные программы наказываются только за то, что они длиннее.
- Поскольку он был разработан в 1976 году, существуют современные структуры (например, лямбда-выражения и такие вещи, как try/catch), которые цикломатическая сложность не может точно объяснить.
- Методы с одинаковыми показателями цикломатической сложности не связаны с тем, насколько сложно их понять или поддерживать, поэтому некоторые из них подчеркиваются, а другие — завышаются.
Это означает, что рассмотрение двух фрагментов кода с одинаковой цикломатической сложностью не означает, что мы имеем дело с двумя фрагментами кода, одинаково простыми для понимания и сопровождения. Например:
```питон
Пример 1
определение суммы простых чисел (макс.):
всего = 0 # + 1
для я в диапазоне (1, макс): # + 1
для j в диапазоне (2, j): # + 1
если я % j == 0: # + 1
перерыв
дж = дж + 1
всего = всего + 1
я = я + 1
return total # Общая цикломатическая сложность = 4
Пример 2
деф getWords(число): # + 1
если число == 1: # + 1
вернуть "один"
число элифов == 2: # + 1
вернуть "пару"
Элиф число == 3: # + 1
вернуть "несколько"
еще:
return "lots" # Общая цикломатическая сложность = 4
Оба этих случая имеют показатель цикломатической сложности 4, но приведенный ниже пример явно чище. Нам нужна метрика, которая может легко различать подобные случаи и давать нам легкое представление о том, прост или сложен код для понимания.
Войдите в когнитивную сложность.Сонар разработал когнитивную сложность (здесь мы использовали некоторые из их примеров, поскольку они являются отличным объяснением некоторых проблем с цикломатической сложностью) в качестве метрики несколько лет назад в качестве ответа на вопрос, легко ли понять код человеку. Он рассчитывает вычислить сложность следующим образом:
- Сокращение или, по крайней мере, отсутствие штрафов за функции, которые позволяют упростить/сокращать код.
- Увеличьте показатель сложности за разрывы в типичном линейном потоке кода — например, циклические структуры, условные операторы или тернарные операторы.
- Увеличьте сложность вложенных разрывов в линейном потоке.
Интуитивно все эти подходы имеют смысл. Условные выражения и циклы естественным образом добавляют уровень сложности, а вложение этих структур еще больше усложняет работу.
Но давайте посмотрим, насколько эти последние примеры, которые мы рассматривали, справедливы с точки зрения когнитивной сложности:
```питон
Пример 1
определение суммы простых чисел (макс.):
всего = 0
для я в диапазоне (1, макс): # + 1
для j в диапазоне (2, j): # + 2
если я % j == 0: # + 3
перерыв
дж = дж + 1
всего = всего + 1
я = я + 1
верните общее количество # Общая когнитивная сложность = 6
Пример 2
деф getWords(число): # + 1
если число == 1: # + 1
вернуть "один"
число элифов == 2: # + 1
вернуть "пару"
Элиф число == 3: # + 1
вернуть "несколько"
еще:
вернуть "много" # Общая когнитивная сложность = 4
Когнитивная сложность больше не рассматривает эти случаи одинаково — вместо этого она говорит, что нижний пример значительно менее сложен, что интуитивно понятно.
Как мы можем повысить когнитивную сложность нашего кода?
Кажется довольно ясным, что когнитивная сложность может использоваться в качестве надежного измерения сложности и удобочитаемости кода и, следовательно, должна иметь четкую обратную связь со скоростью разработки.
Итак, как мы можем приблизиться к повышению когнитивной сложности кода, чтобы упростить его?
Чтобы ответить на этот вопрос, давайте посмотрим на нашего старого доброго друга Gilded Rose. Позолоченная роза имеет (что неудивительно) ужасающую когнитивную сложность 69.
```питон
класс Позолоченная Роза:
Def update_quality(я):
для элемента в self.items: # + 1
если (
item.name != "Выдержанный бри"
и item.name != "Проход за кулисы концерта TAFKAL80ETC"
): # + 2
если item.quality > 0: # + 3
if item.name != "Сульфурас, Рука Рагнароса": # + 4
предмет.качество = предмет.качество - 1
еще:
если item.quality < 50: # + 3
предмет.качество = предмет.качество + 1
if item.name == "Проход за кулисы концерта TAFKAL80ETC": # + 4
если item.sell_in < 11: # + 5
если item.quality < 50: # + 6
предмет.качество = предмет.качество + 1
если item.sell_in < 6: # + 5
если item.quality < 50: # + 6
предмет.качество = предмет.качество + 1
if item.name != "Сульфурас, Рука Рагнароса": # + 2
item.sell_in = item.sell_in - 1
если item.sell_in < 0: # + 2
if item.name != "Выдержанный бри": # + 3
if item.name != "Проход за кулисы концерта TAFKAL80ETC": # + 4
если item.quality > 0: # + 5
if item.name != "Сульфурас, Рука Рагнароса": # + 6
предмет.качество = предмет.качество - 1
иначе: # + 4
предмет.качество = предмет.качество - предмет.качество
еще:
если item.quality < 50: # + 4
предмет.качество = предмет.качество + 1
Общая когнитивная сложность = 69
Ключевым фактором всей этой сложности является 1) огромное количество условных выражений в позолоченной розе и 2) количество вложений, существующих в функции.
Чтобы повысить сложность, мы хотим решить обе эти проблемы.
Давайте просто взглянем на последний раздел «Позолоченной розы», начиная с «if item.sell_in < 0:». Мы можем сделать 3 начальных шага, чтобы добиться больших улучшений здесь:
1. Блок for разделен на три условия if. Сначала рассмотрим последнее. Во вложенном условии проверяется, является ли item.name != "Aged Brie"
.
В общем, легче понять состояние, если тест положительный, а не отрицательный. Поэтому мы начнем с инвертирования условия, что даст следующее:
```питон
если item.sell_in < 0:
if item.name == "Выдержанный бри":
если item.quality < 50:
предмет.качество = предмет.качество + 1
еще:
if item.name != "Проход за кулисы концерта TAFKAL80ETC":
если item.quality > 0:
if item.name != "Сульфурас, Рука Рагнароса":
предмет.качество = предмет.качество - 1
еще:
предмет.качество = предмет.качество - предмет.качество
2. Давайте продолжим и инвертируем условие "Проходы за кулисы" здесь тоже по той же причине.
Это заставляет нас:
```питон
если item.sell_in < 0:
if item.name == "Выдержанный бри":
если item.quality < 50:
предмет.качество = предмет.качество + 1
еще:
if item.name == "Проход за кулисы концерта TAFKAL80ETC":
предмет.качество = предмет.качество - предмет.качество
еще:
если item.quality > 0:
if item.name != "Сульфурас, Рука Рагнароса":
предмет.качество = предмет.качество - 1
3. Затем elif легче читать, чем вложенное if внутри условия else, поэтому давайте изменим его на elif.
```питон
если item.sell_in < 0:
if item.name == "Выдержанный бри":
если item.quality < 50:
предмет.качество = предмет.качество + 1
elif item.name == "Проход за кулисы концерта TAFKAL80ETC":
предмет.качество = предмет.качество - предмет.качество
еще:
если item.quality > 0:
if item.name != "Сульфурас, Рука Рагнароса":
предмет.качество = предмет.качество - 1
В целом это занимает:
```javascript
если item.sell_in < 0: # + 2
if item.name != "Выдержанный бри": # + 3
if item.name != "Проход за кулисы концерта TAFKAL80ETC": # + 4
если item.quality > 0: # + 5
if item.name != "Сульфурас, Рука Рагнароса": # + 6
предмет.качество = предмет.качество - 1
иначе: # + 4
предмет.качество = предмет.качество - предмет.качество
еще:
если item.quality < 50: # + 4
предмет.качество = предмет.качество + 1
К:
```javascript
если item.sell_in < 0: # + 2
if item.name == "Выдержанный бри": # + 3
если item.quality < 50: # + 4
предмет.качество = предмет.качество + 1
elif item.name == "Проход за кулисы концерта TAFKAL80ETC": # + 3
предмет.качество = предмет.качество - предмет.качество
еще:
если item.quality > 0: # + 4
if item.name != "Сульфурас, Рука Рагнароса": # + 5
предмет.качество = предмет.качество - 1
Нам удалось довольно быстро избавиться от 7 пунктов сложности. Следующие шаги начинают усложняться, и Ник подробно описывает их в своей записи в блоге, поэтому я не буду повторить их. Но после нескольких дополнительных шагов мы можем привести позолоченную розу к следующему виду:
```javascript
Def update_quality(я):
для элемента в self.items: # + 1
if item.name == "Сульфурас, Рука Рагнароса": # + 2
Продолжать
if item.name == "Выдержанный бри": # + 2
если item.quality < 50: # + 3
предмет.качество = предмет.качество + 1
если item.sell_in < 1 и item.quality < 50: # + 3
предмет.качество = предмет.качество + 1
elif item.name == "Проход за кулисы концерта TAFKAL80ETC": # + 2
если item.quality < 50: # + 4
предмет.качество = предмет.качество + 1
если item.sell_in < 11 и item.quality < 50: # + 4
предмет.качество = предмет.качество + 1
если item.sell_in < 6 и item.quality < 50: # + 4
предмет.качество = предмет.качество + 1
если item.sell_in < 1: # + 4
предмет.качество = предмет.качество - предмет.качество
еще:
если item.quality > 0: # + 3
предмет.качество = предмет.качество - 1
если item.sell_in < 1 и item.quality > 0: # + 3
предмет.качество = предмет.качество - 1
item.sell_in = item.sell_in - 1
Общая когнитивная сложность = 35
Благодаря этому рефакторингу нам удалось сократить когнитивную сложность с 69 до 35. Это все еще намного выше, чем мы хотели бы (идеальные цели ниже 9), и вы можете продолжать рефакторинг, чтобы продолжать улучшать счет. Постоянно уменьшая вложенность и чрезмерно сложные условные операторы, мы можем значительно улучшить читабельность.
Заключение
Когнитивная сложность дает нам хороший инструмент для выяснения того, насколько сложны различные разделы нашей кодовой базы и, как следствие, какие разделы могут замедлять работу больше всего.
Если мы хотим повысить сложность нашего кода, мы должны сосредоточиться на уменьшении вложенности, ограничении количества условных выражений (особенно вложенных условных выражений), которые мы используем, и использовании сокращений, которые позволяют нам еще больше упростить и сократить наш код.
В конце концов, сложность — это только один из аспектов качества кода, и нам нужно обращать внимание на все из них, чтобы оптимизировать наш код и наши процессы разработки.
Это первая часть серии из семи статей о качестве кода, техническом долге и скорости разработки. Узнайте, как Sourcery может помочь автоматически улучшить и реорганизовать ваш код, чтобы уменьшить технический долг, улучшить качество кода и увеличить скорость.
Оригинал