↓ Перейти к основному содержимому

Стандарты разработки ПО

·867 слов·5 минут
Alexander Sokolov aka s0k0l
Автор
Alexander Sokolov aka s0k0l
Имитирую поведение пользователей, добываю данные и защищаю информацию. Довожу проекты от идеи до релиза.

Это правила, по которым я пишу код. В основе лежит книга Роберта Мартина “Чистый код” и PEP 8 для Python. Документ живой, я дополняю его по мере того, как набиваю шишки.

Главная мысль одна: код читают намного чаще, чем пишут. Поэтому я пишу его для человека, который откроет файл через полгода. Часто этот человек я сам.

0. Без ТЗ результат ХЗ
#

Любая работа начинается с требований, а не с кода.

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

1. Стиль кода: PEP 8 + мои правила
#

PEP 8 это базовый стандарт. Мои правила работают поверх него, как каскад в CSS: все, что я не переопределил, наследуется от PEP 8 как есть. Переопределение одно.

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

Все остальное по PEP 8: длина строки, пустые строки между функциями и классами, порядок импортов, пробелы вокруг операторов.

Чтобы не держать это в голове, стиль проверяет и правит инструмент. Я использую ruff:

# pyproject.toml
[tool.ruff]
line-length = 100

[tool.ruff.format]
indent-style = "tab"

[tool.ruff.lint]
select = ["E", "F", "I", "N", "B", "UP"]
ignore = ["W191"]  # W191 ругается на табы, у нас это осознанное решение

IDE: JetBrains (PyCharm). В настройках проекта включены табы и запуск ruff при сохранении.

2. Имена
#

Имя должно отвечать на три вопроса: зачем это существует, что делает и как используется. Если для имени нужен комментарий, значит имя плохое.

  • Переменные и функции в snake_case, классы в PascalCase, константы в UPPER_CASE.
  • Функции называю глаголом: fetch_page, parse_price, send_report. Классы и переменные существительным: Browser, proxy_pool.
  • Без сокращений и однобуквенных имен. Исключение: i в коротком цикле и e для исключения.
  • Одно понятие, одно слово. Если в проекте есть fetch, то не появляются рядом get, load и retrieve для того же самого.
  • Без кодирования типа в имени: users, а не users_list или lst_users.
  • Булевы переменные читаются как вопрос: is_ready, has_proxy, can_retry.

3. Функции
#

  • Маленькие. Функцию должно быть видно целиком на экране без прокрутки. Если не видно, ее можно разбить.
  • Делают одно дело. Если функцию нельзя описать одним предложением без слова “и”, это две функции.
  • Один уровень абстракции. Функция либо управляет процессом и вызывает другие функции, либо работает с деталями. Не одновременно.
  • Мало аргументов. Идеально ноль-два. Три и больше это повод собрать их в dataclass.
  • Без флагов в аргументах. render(page, True) непонятно читается. Лучше две функции: render_full и render_preview.
  • Без скрытых побочных эффектов. Если функция называется check_session, она не должна по дороге создавать новую сессию.
  • Команда или запрос. Функция либо меняет состояние, либо возвращает данные. Не то и другое сразу.

4. Комментарии
#

Лучший комментарий это тот, который не понадобился, потому что код и так понятен.

Пишу комментарий, когда нужно объяснить почему, а не что:

# Cloudflare отдает 403 на первый запрос без cookie, поэтому всегда делаем два
response = session.get(url)

Не пишу:

  • комментарии, которые пересказывают код
  • закомментированный код. Для истории есть git
  • журнал изменений и авторство в шапке файла. Это тоже работа git

Docstring обязателен для публичных функций и классов модуля. Для внутренних по ситуации.

5. Обработка ошибок
#

  • Исключения вместо кодов возврата. Функция не возвращает None или -1, если что-то пошло не так, она бросает исключение.
  • Ловлю конкретные исключения, никогда голый except: или except Exception: pass.
  • Свои исключения для своей предметной области: ProxyBannedError, CaptchaError. По ним понятно, что случилось, без чтения стектрейса.
  • Не возвращаю None там, где ждут коллекцию. Пустой список лучше, чем проверка на None в каждом месте вызова.
  • Ошибку обрабатываю там, где знаю, что с ней делать. Если не знаю, пропускаю выше.

6. Классы и модули
#

  • Маленькие классы с одной ответственностью. У класса должна быть одна причина для изменения. Если класс и ходит в сеть, и парсит HTML, и пишет в базу, это три класса.
  • Высокая связность. Методы класса работают с его полями. Если метод не трогает self, ему, скорее всего, не место в этом классе.
  • Зависимости снаружи. Класс получает браузер, сессию или клиент базы в конструкторе, а не создает их сам. Так его легко тестировать и подменять.
  • Закон Деметры. Общаюсь только с ближайшими объектами. order.customer.address.city это знак, что нужен метод.
  • Границы с чужим кодом. Сторонние библиотеки оборачиваю в свой тонкий слой. Если библиотеку придется заменить, правка будет в одном месте.

7. Тесты
#

  • Тесты это тоже код и к ним те же требования к чистоте.
  • Тест проверяет одну вещь, и это видно по его имени: test_returns_empty_list_when_page_has_no_items.
  • Структура каждого теста: подготовка, действие, проверка.
  • Тесты быстрые, независимые друг от друга и дают одинаковый результат при каждом запуске. Никаких походов в реальный интернет в юнит-тестах.
  • Баг сначала воспроизвожу тестом, потом чиню.

8. Правило бойскаута
#

Оставляю код чище, чем он был до меня. Не нужно переписывать все сразу. Достаточно переименовать одну непонятную переменную или вынести один кусок в функцию каждый раз, когда трогаю файл.

9. Git
#

  • Один коммит, одно логическое изменение.
  • Сообщение коммита в формате conventional commits: feat:, fix:, refactor:, docs:, chore:.
  • В основную ветку не попадает код, который не проходит линтер и тесты.
  • Секреты, ключи и .env никогда не попадают в репозиторий.