Рубрики
Без рубрики

Сложность водопада

Первоначально опубликовано в моем блоге: https://sobolevn.me/2019/10/Comeblevn.me/2019/10/Complexity-waverfall.

Первоначально опубликовано в моем блоге : https://sobolevn.me/2019/10/complexity-waterfall

Говоря о «плохом коде», люди почти наверняка означают «сложный код» среди прочих популярных проблем. Дело в сложности состоит в том, что она выходит из ниоткуда. Однажды вы начинаете свой довольно простой проект, на днях вы находите его в руинах. И никто не знает, как и когда это произошло.

Но это в конечном итоге случается по причине! Кодовая сложность входит в вашу кодовую базу двумя возможными способами: с большими кусками и дополнительными дополнениями. И люди плохие при рассмотрении и поиска их обоих.

Когда приходит большой кусок кода, рецензент будет оспариваться, чтобы найти точное местоположение, в котором код сложен и что делать с этим. Затем обзор придется доказать точку: почему этот код является сложным в первую очередь. И другие разработчики могут не согласиться. Мы все знаем такие виды кода отзывы!

Второй способ сложности, попадающей в ваш код, является инкрементным добавлением: при отправке одной или две строки в существующую функцию. И крайне сложно заметить, что ваша функция была в порядке, но теперь это слишком сложно. Требуется хорошая часть концентрации, рассмотрение навыков и хорошей практики навигации по коду, чтобы на самом деле обнаружить ее. Большинство людей (как я!) Не хватает этих навыков и позволяют регулярно войти в кодовую базу.

Итак, что можно сделать, чтобы предотвратить комплекс вашего кода? Нам нужно использовать автоматизацию! Давайте сделаем глубокое погружение в сложность кода и способы найти и, наконец, решить ее.

В этой статье я направляю вас через места, где живет сложность и как туда драться. Тогда мы обсудим, насколько хорошо написано простой код и автоматизация позволяют «непрерывному рефакторингу» и «архитектуре по требованию» стилями развития.

Объяснение сложности

Можно спросить: что именно “кодовая сложность” есть? И хотя он звучит знакомо, есть скрытые препятствия в понимании точной локации сложности. Давайте начнем с самых примитивных частей, а затем перейти на объекты более высокого уровня.

Помните, что эта статья называется «Водопад сложности»? Я покажу вам, как сложность из простейших примитивов переполняется в самые высокие абстракции.

Я буду использовать Python Как основной язык для моих примеров и Wemake-Python-Stureguide Как главный подвижный инструмент, чтобы найти нарушения в моем коде и проиллюстрировать мою точку зрения.

Выражения

Весь ваш код состоит из простых выражений, таких как A + 1 и Печать (х) . Хотя выражения их просты, они могут быть незаметно переполнены ваш код со сложностью в какой-то момент. Пример: представьте, что у вас есть словарь, который представляет некоторые Пользователь модель и вы используете это так:

def format_username(user) -> str:
    if not user['username']:
        return user['email']
    elif len(user['username']) > 12:
        return user['username'][:12] + '...'
    return '@' + user['username']

Это выглядит довольно просто, не так ли? Фактически, он содержит два вопроса сложности на основе выражений. Это Орехи «Имя пользователя» Строка и использует волшебный номер 12 (Почему мы используем это число в первую очередь, почему бы не 13 или 10 ?). Трудно сами трудно найти такие вещи. Вот как будет выглядеть лучшая версия:

#: That's how many chars fit in the preview box.
LENGTH_LIMIT: Final = 12

def format_username(user) -> str:
    username = user['username']

    if not username:
        return user['email']
    elif len(username) > LENGTH_LIMIT:  # See? It is now documented
        return username[:LENGTH_LIMIT] + '...'
    return '@' + username

Есть разные проблемы с выражением. Мы также можем иметь чрезмерные выражения : Когда вы используете quey_object.some_attr атрибут везде вместо создания новой локальной переменной. Мы также можем иметь Слишком сложные логические условия или Слишком глубокий точковый доступ Отказ

Решение : Создайте новые переменные, аргументы или константы. Создайте и используйте новые функции или методы утилиты, если вам нужно.

Линией

Выражения формы Code Lines (пожалуйста, не путайте строки с заявлениями: однократное утверждение может предпринять несколько строк, а несколько утверждений могут быть расположены на одной строке).

Первая и самая очевидная метрика сложности для линии – его длина. Да, вы слышали это правильно. Вот почему мы (программисты) предпочитают придерживаться 80 Правило Chars-Per-Line и не потому, что это было ранее использовал в телеэтаптеров. В последнее время много слухов в последнее время, говоря, что это не имеет никакого смысла использовать 80 Chars для вашего кода в 2k19. Но это явно не правда.

Идея проста. Вы можете иметь в два раза больше логики в строке с 160 Чарс, чем в соответствии с только 80 Чарс. Вот почему этот предел должен быть установлен и применен. Помните, это не стилистический выбор Отказ Это сложность метрики!

Вторая основная метрика сложности линии менее известна и менее используется. Это называется Сложность Джонса Отказ Идея позади нее проста: мы считаем узлы кода (или AST ) в одной строке, чтобы получить его сложность. Давайте посмотрим на пример. Эти две линии принципиально отличаются с точки зрения сложности, но имеют одинаковую ширину в символах:

print(first_long_name_with_meaning, second_very_long_name_with_meaning, third)
print(first * 5 + math.pi * 2, matrix.trans(*matrix), display.show(matrix, 2))

Давайте посчитаем узлы в первом: один звонок, три имена. Четыре узла полностью. Второй имеет двадцать один AST узлы. Ну, разница ясна. Вот почему мы используем метрику «Сложность Джонса», чтобы позволить первой длинной линии и запретить вторую на основе внутренней сложности, а не на необработанной длине.

Что связано с линиями с высокой оценкой сложности Jones?

Решение : Разделите их на несколько строк или создавайте новые промежуточные переменные, функции утилиты, новые классы и т. Д.

print(
    first * 5 + math.pi * 2,
    matrix.trans(*matrix),
    display.show(matrix, 2),
)

Теперь это намного читаемое!

Структуры

Следующий шаг анализирует языковые структуры, такие как Если , для , с и т. Д., Образуются из линий и выражений. Я должен сказать, что эта точка очень относится к языку. Я продемонстрирую несколько правил из этой категории, используя Python также.

Мы начнем с Если Отказ Что может быть проще, чем хороший Если ? На самом деле, Если начинает становиться хитроэ очень быстро. Вот пример того, как можно Reimplement Переключатель с Если :

if isinstance(some, int):
    ...
elif isinstance(some, float):
    ...
elif isinstance(some, complex):
    ...
elif isinstance(some, str):
    ...
elif isinstance(some, bytes):
    ...
elif isinstance(some, list):
    ...

В чем проблема с этим кодом? Ну, представьте, что у нас есть десятки типов данных, которые должны быть покрыты, включая таможенные, которые мы еще не знаем. Тогда этот сложный код является индикатором, который мы выбираем здесь неправильный шаблон. Нам нужно решить наш код, чтобы исправить эту проблему. Например, можно использовать TypeClass. es или Singledispatch. . Они та же работа, но приятнее.

Python Никогда не останавливается, чтобы развлечь нас. Например, вы можете написать с с произвольное количество случаев , что слишком умственно сложно и запутает:

with first(), second(), third(), fourth():
    ...

Вы также можете записать возможности с любым количеством Если и для Выражения, которые могут привести к сложному, нечитаемым коде:

[
    (x, y, z)
    for x in x_coords
    for y in y_coords
    for z in z_coords
    if x > 0
    if y > 0
    if z > 0
    if x + y <= z
    if x + z <= y
    if y + z <= x
]

Сравните его с простым и читаемым версией:

[
    (x, y, z)
    for x, y, x in itertools.product(x_coords, y_coords, z_coords)
    if valid_coordinates(x, y, z)
]

Вы также можете случайно включать Несколько утверждений внутри попробуйте Чехол, небезопасный, потому что он может поднять и обращаться с исключением в ожидаемом месте:

try:
    user = fetch_user()  # Can also fail, but don't expect that
    log.save_user_operation(user.email)  # Can fail, and we know it
except MyCustomException as exc:
    ...

И это даже не 10% случаев, которые могут и будут не так с вашим Python код. Есть много, много Больше корпусов края это должно отслеживать и проанализировать.

Решение : Единственное возможное решение – использовать хороший линт для языка по вашему выбору. И рефакторист Комплексные места, что этот линт подсвечивает. В противном случае вам придется изобретать колесо и настроить пользовательские политики в течение той же проблем.

Функции

Выражения, заявления и структуры формируются функции. Сложность от этих объектов течет в функции. И вот где все начинают интригуют. Поскольку функции имеют буквально десятки метрик сложности: как хорошие, так и плохие.

Мы начнем с самых известных: цикломатическая сложность И длина функции, измеренная в строках кода. Цикломатическая сложность указывает на то, сколько превращает ваш исполнительный поток: он почти равен количеству единичных тестов, которые необходимы для полного покрытия исходного кода. Это хорошая метрика, потому что он уважает семантический и помогает разработчику сделать рефакторинг. С другой стороны, длина функции – плохая метрика. Он не работает с ранее объясненными метрикой сложности Джонса, поскольку мы уже знаем: несколько строк легче читать, чем одна большая линия со всем внутри. Мы сосредоточимся на хороших показателях метрики и игнорируем плохие.

Основываясь на моем опыте, несколько полезных метрик сложности следует учитывать вместо продолжительности обычной функции:

  • Количество функциональных декораторов; ниже – лучше
  • Количество аргументов; ниже – лучше
  • Количество аннотаций; выше лучше
  • Количество локальных переменных; ниже – лучше
  • Количество доходных доходов, ждет; ниже – лучше
  • Количество утверждений и выражений; ниже – лучше

Сочетание всех этих проверок действительно позволяет писать простые функции (все правила также применяются к способам).

Когда вы постараетесь сделать некоторые неприятные вещи с вашей функцией, вы, наверняка сломаете хотя бы одну метрику. И это разочарует наш линт и ударить вашу сборку. В результате ваша функция будет сохранена.

Решение : Когда одна функция слишком сложная, единственное решение, которое у вас есть, – это разделить эту функцию на несколько.

Классы

Следующий уровень абстракции после функций – это классы. И как вы уже догадались, они еще более сложны и жидкости, чем функции. Поскольку классы могут содержать несколько функций внутри (которые называются методом) и имеют другие уникальные функции, такие как наследование, и микшины, атрибуты классов и декораторы уровня класса. Итак, мы должны проверить все методы в качестве функций и самого класса.

Для классов мы должны измерить следующие метрики:

  • Количество декораторов классов; ниже – лучше
  • Количество базовых классов; ниже – лучше
  • Количество публичных атрибутов классов; ниже – лучше
  • Количество публичных атрибутов на уровне экземпляра; ниже – лучше
  • Количество методов; ниже – лучше

Когда любой из них чрезмерно сложный – мы должны позвонить в тревогу и выйти из строя!

Решение : Refactor ваш неудачный класс! Разделите один существующий сложный класс на несколько простых или создайте новые функции утилиты и используйте композицию.

Примечательно упомянуть: можно также отследить Сплоченность и сцепление Метрики Чтобы подтвердить сложность вашего дизайна ООП.

Модули

Модули содержат несколько утверждений, функций и классов. И, как вы, возможно, уже упоминали, мы обычно советуем разделить функции и классы в новые. Вот почему мы должны держать и следить за сложностью модуля: он буквально течет в модули из классов и функций.

Для анализа сложности модуля мы должны проверить:

  • Количество импорта и импортированных имен; ниже – лучше
  • Количество классов и функций; ниже – лучше
  • Средняя сложность функций и классов внутри; ниже – лучше

Что мы делаем в случае сложного модуля?

Решение : Да, вы поняли это правильно. Мы разделили один модуль на несколько.

Пакеты

Пакеты содержат несколько модулей. К счастью, это все, что они делают.

Итак, он Количество модулей в пакете может скоро начать быть слишком большим, поэтому вы окажетесь слишком многими из них. И это единственная сложность, которую можно найти с пакетами.

Решение : Вы должны разделить пакеты в подпаусы и пакеты разных уровней.

Сложность водопада эффект

Теперь мы охватываем почти все возможные виды абстракций в вашей кодовой базе. Что мы узнали от этого? На данный момент основной вынос в том, что большинство проблем могут быть решены с извлечения сложности к тому же или верхней части абстракции.

Это приводит нас к самому важному представлению этой статьи: не позволяйте вашему коду переполнено сложностью. Я дам несколько примеров того, как это обычно происходит.

Представьте, что вы реализуете новую функцию. И это единственное, что вы делаете:

+++ if user.is_active and user.has_sub() and sub.is_due(tz.now() + delta):
--- if user.is_active and user.has_sub():

Смотри хорошо, я бы передал этот код на обзор. И ничего плохого не произойдет. Но точка, которую я скучаю, это то, что сложность переполнена этой линией! Вот что Wemake-Python-Stureguide будет сообщать:

Хорошо, теперь мы должны решить эту сложность. Давайте сделаем новую переменную:

class Product(object):
    ...

    def can_be_purchased(self, user_id) -> bool:
        ...

        is_sub_paid = sub.is_due(tz.now() + delta)
        if user.is_active and user.has_sub() and is_sub_paid:
            ...

        ...

...

Теперь сложность линии решена. Но подожди минутку. Что если наша функция имеет слишком много переменных сейчас? Поскольку мы создали новую переменную без проверки их номера внутри функции. В этом случае нам придется разделить этот метод на несколько таких как:

class Product(object):
    ...

    def can_be_purchased(self, user_id) -> bool:
        ...

        if self._has_paid_sub(user, sub, delta):
            ...

        ...

    def _has_paid_sub(self, user, sub, delta) -> bool:
        is_sub_paid = sub.is_due(tz.now() + delta)
        return user.is_active and user.has_sub() and is_sub_paid

...

Теперь мы закончили! Правильно? Нет, потому что теперь мы должны проверить сложность Продукт класс. Представьте, что у него сейчас слишком много методов, так как мы создали новый _has_paid_sub один.

Хорошо, мы бегаем наш линтер, чтобы снова проверить сложность. И получается наш Продукт Класс действительно слишком сложен прямо сейчас. Наши действия? Мы разделили его на несколько классов!

class Policy(object):
    ...

class SubcsriptionPolicy(Policy):
    ...

    def can_be_purchased(self, user_id) -> bool:
        ...

        if self._has_paid_sub(user, sub, delta):
            ...

        ...

    def _has_paid_sub(self, user, sub, delta) -> bool:
        is_sub_paid = sub.is_due(tz.now() + delta)
        return user.is_active and user.has_sub() and is_sub_paid

class Product(object):
    _purchasing_policy: Policy

    ...

...

Пожалуйста, скажите, что это последняя итерация! Ну, я сожалею, но теперь мы должны проверить сложность модуля. И угадайте, что? Теперь у нас слишком много членов модуля. Итак, мы должны разделить модули в отдельных! Затем мы проверяем сложность пакета. А также, возможно, разделить его на несколько субкарпаков.

Вы видели это? Из-за четко определенной правил сложности наша однострочная модификация оказалась огромной рефакторинговой сессией с несколькими новыми модулями и классами. И мы сами не приняли ни одного решения: все наши цели рефакторинга были обусловлены внутренней сложностью и линтом, который раскрывает его.

Это то, что я называю процесс «непрерывный рефакторинг». Вы вынуждены сделать рефакторинг. Всегда.

Этот процесс также имеет одно интересное следствие. Это позволяет вам иметь «архитектуру по требованию». Позволь мне объяснить. С философией «Архитектура по требованию» вы всегда начинаете маленькие. Например с одним Логика/Домены/user.py файл. И вы начинаете ставить все Пользователь связанный там. Потому что в этот момент вы, вероятно, не знаете, как будет выглядеть ваша архитектура. И тебе все равно. У вас всего три функции.

Некоторые люди вступают в архитектуру VS Code Commacity Trap. Они могут чрезмерно усложнять свою архитектуру с самого начала с полного хранилища/слоев обслуживания/домена. Или они могут чрезмерно усложнить исходный код без четкого разделения. Борьба и жить так, как это годами (если они смогут жить в течение многих лет с таким кодом!).

Концепция «Архитектура по требованию» решает эти проблемы. Вы начинаете маленькие, когда придет время – вы разбите и рефакторуете вещи:

  1. Вы начинаете с Логика/Домены/user.py и положить все там
  2. Позже вы создаете логика/домены/пользователь/Repository.py Когда у вас достаточно базы данных Связанные вещи
  3. Тогда вы разделили его в Логика/Домены/Пользователь/Репозиторий/Queries.py и Логика/Домены/Пользователь/Репозиторий/Commands.py Когда сложность говорит вам сделать это
  4. Тогда вы создаете логика/домены/пользователь/services.py с http . Связанные вещи
  5. Тогда вы создаете новый модуль под названием логика/домены/заказ.
  6. И так далее, и так далее

Вот и все. Это идеальный инструмент для уравновешивания вашей архитектуры и сложности кода. И получить столько архитектуры, поскольку вы действительно нуждаетесь в на данный момент.

Вывод

Хороший линт гораздо больше, чем находятся пропущенные запятые и плохие цитаты. Хороший литер, позволяет вам полагаться на это с помощью архитектурных решений и помочь вам с процессом рефакторинга.

Например, Wemake-Python-Stureguide может помочь вам с Python Сложность исходного кода, это позволяет:

  • Успешно бороться с сложностью на всех уровнях
  • Обеспечить огромное количество стандартов именования, лучших практик и проверки согласованности
  • Легко интегрировать его в устаревшую базу кода с помощью Различать Опция или flakeell Инструмент, так старое нарушение будет прощено, но новые не будут допущены
  • Включите его в свой CI даже как Действие GitHub

Не позволяйте сложности переполнить ваш код, Используйте хороший Linter Действительно

Оригинал: “https://dev.to/wemake-services/complexity-waterfall-n2d”