О плохом и хорошем коде

Моя цель - предложение широкого ассортимента товаров и услуг на постоянно высоком качестве обслуживания по самым выгодным ценам.

Прежде чем перейти к статье, хочу вам представить, экономическую онлайн игру Brave Knights, в которой вы можете играть и зарабатывать. Регистируйтесь, играйте и зарабатывайте!

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

Давайте для начала договоримся об используемых терминах.

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

Архитектура: дополнительный код, который не относится напрямую к решению задачи.

Бизнес логика: код, который решает задачу бизнеса.

Низкоуровневая логика: Код, который делает реальную работу. Сюда попадает установка соединений, сетевые запросы, парсинг JSON, всякая работа с байтами. В общем, все то, чему учат программиста.

Варианты решения

Обработку данных я специально упростил. В жизни объекты сложнее и обработка может быть достаточно сложной.

Вариант 1. Сделал дело - гуляй смело

session = requests.Session()
session.post("https://prod.com/login", json={"user": "u", "password": "p"})

data = session.get("https://prod.com/data")
data = json.loads(data)
updated_data = [field.upper() for field in data]
session.post("https://prod.com/data", json=updated_data)

Более продвинутая версия - это завернуть этот код в функцию main. Но это практически ничего не меняет в лучшую сторону.

Этот код является хорошим, когда у вас нет публикации.

Я так пишу в следующих случаях:

  • Нужно сделать одноразовую работу и выкинуть этот скрипт. Кроме меня его никто не увидит.

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

Что хорошо:

  • Это самое минимальное количество кода, которое можно написать.

Что плохо:

  • Чтобы понять придётся читать всё от корки до корки, потому что бизнес-логика и низкоуровневая логика перемешаны.

  • Попытка писать юниттесты на такой код - боль. Можно заработать отвращение к тестам.

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

Варинат 2. Вперёд к светлому будущему

def download_data(session):
  data = session.get("https://prod.com/data")
  return json.loads(data)


def process_data(data):
  return [field.upper() for field in data]


def upload_data(session, updated_data):
  session.post("https://prod.com/data", json=updated_data)


def get_session():
  session = requests.Session()
  session.post("https://prod.com/login", json={"user": "u", "password": "p"})
  return session


def main():
  session = get_session()
  data = download_data(session)
  updated_data = process_data(data)
  upload_data(session, updated_data)

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

Что хорошо:

  • Бизнес-логика и низкоуровневая логика разделены и код можно читать как справочник, по частям. Прочитав оглавление (main) можно решить, стоит ли заглядывать в отдельные главы. process_data - это бизнес-логика, туда стоит посмотреть повнимательней. Остальные части - низкоуровневая логика.

  • Сложность каждой отдельной части очень небольшая, каждая функция сфокусирована на отдельной задаче.

  • Тестируемость кода сильно возрасла. Появилась возможность частично тестировать код. Покрыв 100% process_data тестами и протестировав остальное рукуми мы получим очень неплохой результат. Остальные методы обычно меняются значительно реже чем бизнеслогика, а тестировать их значительно сложнее.

  • Появилась возможность совместно работать на кодом. Например, мы можем паралелльно менять бизнес-логику и транспортный протокол.

Что плохо:

  • Нужно писать больше кода.

С современными средствами разработки, переход от прототипа к структуированному коду - дело нескольких минут. Расходы на дополнительный код с лихвой окупаются положительными эффектами. Когда мне нужно разобраться с каким-то кодом на сотни строк, я часто сначала привожу его к такому виду. Это почти механический процесс, который не сильно нагружает мозг.

Вариант 3. Сириус бизнес

def make_downloader(session):
  def wrapper():
    data = session.get("https://prod.com/data")
    return json.loads(data)
  return wrapper


def process_data(data):
  return [field.upper() for field in data]


def make_uploader(session):
  def wrapper(updated_data):
    session.post("https://prod.com/data", json=updated_data)
  return wrapper


def application(downloader, processor, uploader):
  data = downloader()
  updated_data = processor(data)
  uploader(updated_data)


def get_session():
  session = requests.Session()
  session.post("https://prod.com/login", json={"user": "u", "password": "p"})
  return session


def main():
  session = get_session()
  downloader = make_downloader(session)
  uploader = make_uploader(session)
  application(downloader, process_data, uploader)

Стало сильно сложнее, давайте раберёмся когда это имеет смысл. Такой код оправдан, если мы знаем, что будем долго и счастливо его расширять. Получается такой узкоспециализированный фреймворк.
Если у вас есть возможность использовать готовый фреймворк, то изобретать велосипед тоже не стоит.

Что хорошо:

  • Бизнес-логика полностью отделена от низкоуровневой. Теперь можно протестировать как мы связываем все части воедино через простые заглушки. Легко писать тесты, значит, легко менять код. Из-за хорошего разделения, проще контролировать на что влияют изменения.

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

  • Появляется возможность создавать много точек входа. То есть, если у вас есть несколько окружений для запуска кода, вы можете написать свой main для каждого.

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

  • Много можно сделать не запуская всю систему, это порой сильно экономит время.

Что плохо:

  • Добавочная сложность вполне увесистая. В общем, создание собственных фреймворков, даже если они микро никогда не было простым решением. Архитектурного кода может оказаться значительно больше чем бизнес-логики.

  • Для разработки ядра фреймворк стоит иметь опытного специалиста.

Немного о реализации.

Архитектура состоит из двух уровней: main собирает объекты, а application уже делает нужную работу. В примере реализована ручная инъекция зависимостей, в жизни же может быть другая конструкция, например в Django аналогами main и application являются settings.py и apps.py.


Более подробно расскажу о тестировании. Протестируем сердце нашего кода - application.

def test_application():
  downloader = lambda _: ['a', 'b', 'c']
  uploader = Mock()
  application(downloader, process_data, uploader)
  uploader.assert_called_with(['A', 'B', 'C'])

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

С ростом приложения, будет расти количество аргументов, для того, чтобы в них не утонуть, стоит строить дерево зависимостей в высоту. В данном случае session напрямую не передаётся в приложение, потому, что они находится на уровень ниже.

Архитектура нужна для удобства расширения и поддержки кода.
Давайте проверим этот код на гибкость и поддерживаемость.

  • Нужно скачивать не с сервера а из базы данных: добавляю модуль с чтением из базы, добавляю новую точку входа (main). Есть только расширение и никакого изменения.

  • Нужно поддерживать загрузку в два места одновременн: на сервер и в очередь сообщений. Добавляю новый загрузчик в очередь сообщений. Добавляю еще один загрузчик, который вызывает загрузчик на сервер и в очередь (паттерн компоновщик, composite). Создаю новую точку входа, где использую второй загрузчик. Есть только раширение и никакого изменения.

  • Загрузчик в сеть больше стал не нужен. Удаляю файл, удаляю точку входа. В остальных файлах нет изменений.

  • Нужно поправить ошибку в загрузчике. Меняем файл загрузчика. Проблема чётко локализована в одном модуле.

  • Меняем контракт для загрузки данных, теперь нужно передавать не только объект но и метаданные. Как только меняются интерфейсы - это боль. В данном случае нужно поменять приложение, нужно поменять все реализации загрузчиков. Ну и всё перетестировать. Хотя можно схитрить: просто сделать второе приложение и использовать новые загрузчики с ним.


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

Немного опыта из личной жизни

С написанием больших фреймворков по такой схеме я не сталкивался. Либо использовал готовые, либо фреймворк уже был до меня. А вот несколько маленьких написал и был этим доволен. В одном случае это были AWS лямбды, во втором - код, который выполняется внутри С++ процесса. И тот и другой требовали некоторое количество действий если тестировать вручную.
В обоих случаях на 100% покрыл логическую часть тестами. Интеграцию тестировал руками, но она практически не менялась со временем. Запуск юниттестов занимал пол секунды, и была возможность локального дебага.

Лямбды (коммерческая разработка)

В микросервисной архитектуре отвечал за несколько сервисов реализованных с помощью AWS Lambda. Использовал в них код похожий на третий пример.

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

В лямбде изменений было достаточно много, и интерфейсы серьёзно менялись, и нестыковки с требованиями были. Я менял код достаточно агрессивно и быстро, потому что верил, что тесты всё поймают. Для уверенности в тестовом покрытии смотрел детальный отчёт по покрытию кода.

Самые большие тесты на внешний уровень были строчек на 20ть из них: 10 - создание заглушек и входных данных, одна строка вызова, 10 строк проверки, что заглушки вызваны. В основном же это было 3-4 строки. Всего тестов было около сотни. Я активно использую параметризованные тесты, поэтому самих тестовых функций было поменьше.

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

Код внутри рантайма

Пошаговая стратегия, внутри есть чат с игроками. Для удобства разработки добавил возможность поговорить с компьютерным игроком и открыть REPL в его состояние. Такой интерактивный дебаг для ленивых. Процесс AI как и С++ API которое он исползует существуют только в рантайме. Я взаимодействие с API вынес в отдельный модуль и логику стало возможно запускать локально. https://github.com/freeorion/freeorion/tree/master/default/python/AI/freeorion_tools/chat_handler

Сами тесты получились вполне короткими, но с фикстурами пришлось повозиться. https://github.com/freeorion/freeorion/blob/master/default/python/tests/AI/test_debug_chat.py#L129

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

Заключение

Каждому коду есть свой контекст. Написать фреймворк, вместо скрипта на 100 строк или запихать кучу бизнес-логики одной функцией на тысячу строк это явные промахи.

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

Юнит-тесты - очень хороший показатель архитектуры. Если получается писать простые юнит-тесты, значит в проекте модельность соответствует сложности.

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

Источник: https://habr.com/ru/post/666310/


Интересные статьи

Интересные статьи

Энкодер - это устройство преобразования механического перемещения или угловых изменений положения в цифровой сигнал. В статье рассматривается самый популярный в DIY сообществе инкрементальный энкодер...
Выгрузка пользователей из 1C ЗУП в Битрикс24 или правдивая история о том как настроить интеграцию 1С-Битрикс24 с ЗУП без 1С-ника.В жизни так бывает, причём бывает чаще чем хотелось бы, хоть в целом и ...
Всем привет! В этой статье я расскажу про процесс автоматизации изображений в коде, написание гема и ускорение рутинных задач. Читать далее
Как часто вы работали с 7-сегментными индикаторами? Вам не надоедало, что для каждого из них, надо тянуть пачку проводов? В этот статье мы попробуем решить данную проблем...
Чтение исходного кода известных фреймворков может хорошо помочь программисту в улучшении его профессиональных навыков. Автор статьи, перевод которой мы сегодня публикуем, недавно анал...