Почему к моим совета стоит прислушаться
Александр Чепайкин
Senior Python Developer в крупном финтехе. С 2012 года в IT, участвовал в разработке простых сайтов, интернет-магазинов, игр и сложных распределенных систем. Несколько лет работал удаленно на Кремниевую долину в крупных стартапах.
Я активно участвую в обучении начинающих программистов и на регулярной основе помогаю ребятам делать тестовые задания (далее ТЗ) и готовиться к собеседованиям. Не просто видел, а постоянно вижу много вариантов заданий, их решений, историй успехов и неудач.
Также рекомендую посмотреть этот Разбор тестовых заданий (Python начинается с 27:36)
Оглавление
Введение
В статье я буду говорить про Python, но многие советы будут полезны для ТЗ на любом языке программирования.
Я специально не привожу ссылок на готовые решения, чтобы оставить вам пространство для самореализации и недопустить, чтобы все начали делать тестовые задания совсем уж одинаково. Когда работодатель видит, что много решений похожи друг на друга, то сразу понятно, что где-то это было списано\подсмотрено, и отдает предпочтение более уникальным решениям.
Эта статья не для галочки. Я планирую постоянно ей пользоваться, давая её как гайд тем, кто обращается ко мне с вопросами по ТЗ. В этой статье мы не будем разбирать конкретное решение, инструменты и библиотеки все это тянет на отдельные серии статей. Если будет много запросов на это, то возможно, я напишу более подробную серию статей.
Статья рассчитана на уровень Junior, но может быть полезна и для разработчиков с опытом, которые давно не делали тестовые задания, по крайней мере успешно.
Мы будем рассматривать самые частые ошибки встречающиеся в ТЗ, как их на самом деле проверяют, и почему вы чаще всего не получаете обратной связи по своему решению.
Многое из статьи может показаться сложным или излишним. Тут я сразу отвечу, что вы должны воспринимать ТЗ как конкурсную работу, где побеждает лучший. Важно показать максимум своих знаний. Просто формально решить задание недостаточно, особенно сейчас, когда на одну вакансию junior 1000+ кандидатов.
Даже если что-то явно не требуется в ТЗ это не означает, что работодатель этого не ожидает, или что это не сделает кто-то из ваших конкурентов на вакансию, оставив вас позади.
Работа с заданием
Внимательно изучите задание. Самый частый случай несоблюдения требований, это когда кандидат долгое время, работая над заданием, уже его не перечитывает и не сверяет свое решение с изначальными требованиями. Просто пребывает в иллюзии что он его помнит.
Часто в задании указывается, что выполнение каких-то действий будет являться плюсом. Эти пожелания лучше рассматривать как обязательные.
Обязательно перед отправкой решения на проверку перечитайте задание и убедитесь, что вы все сделали так, как требовалось.
README.md
Чаще всего ТЗ вы будете делать в репозитории GitHub, но даже если нет, вам все равно нужно оформить README.md. Все должно быть оформлено красиво используя Markdown. Как минимум это НЕ должен быть просто сплошной текст без форматирования.
Он должен содержать:
Название и описание (кратко) сервиса
(Опционально) Необходимые зависимости. Не pip зависимости сервиса а скорее окружения в котором оно запускается. (версия python, postgresql или только docker, docker compose). Возможно в вашем случае можно добавить что то еще например, если это ML то можно указать сколько нужно GPU для запуска и т.д.
Инструкция по запуску сервиса и проверки его работы. Обычно Docker Compose, и можно добавить Postman коллекцию с API. Обязательно проверьте что ваш проект можно развернуть с нуля по этой инструкции, и если чего то не хватает, то добавьте.
Не очень много правда? У большинства этого нет. Ваша задача в README не нагрузить его информацией, а сделать проверку вашей работы максимально простой.
Если мне нужно проверить 50 ТЗ в первую очередь я буду смотреть те, где есть оформленное README. Это говорит о том, что человек старался и я возможно быстрее найду достойного кандидата.
Многие работодатели даже не проверяют ТЗ если у него нет README с инструкцией запуска. Иногда об этом прямо пишут в ТЗ, но даже если нет, относитесь к этому как к обязательному требованию.
Docker
Если в вашем ТЗ это применимо (бэкенд всегда применимо), то обязательно заверните ваше решение в Docker. Чаще всего для этого достаточно создать Dockerfile для вашего сервиса и Docker Compose файл с БД PostgreSQL (если другая БД не требуется в задании) и вашим сервисом.
Не буду приводить конкретных примеров, тут вам легко поможет гугл или ChatGPT.
Структура проекта
Продумывайте структуру вашего проекта. Если не знаете как декомпозировать код, смотрите как это делают другие особенно на примере чистой архитектуры и DDD.
Если чувствуете, что вы организуете код не системно, а просто как получится, то скорее всего тут будут проблемы. Точно нет смысла писать ТЗ на Flask или FastAPI в одном файле. Разделите ваш код на слои как минимум вам нужно создать слой Service и Repository и соблюдать все принципы SOLID. Тут придется потратить время, чтобы в этом разобраться, но сделав это один раз, вы потом всегда будете писать ТЗ с хорошей организацией кода, где будет виден системный подход. В ТЗ как правило не требуется полное соблюдение чистой архитектуры или DDD, но если сроки позволяют, то это будет существенным плюсом.
Пример ниже не претендует на идеал и единство верный вариант. Я лишь хочу сказать, что в организации кода должна прослеживаться системность, и хотя бы минимальное разделение кода на слои. Не должно быть все, или почти все, в одном файле или папок которые содержат случайные по назначению файлы.
Пример структуры ТЗ
├── alembic.ini
├── docker-compose.yml
├── Dockerfile
├── README.md
├── tests
│ ├── conftest.py
│ ├── __init__.py
│ ├── test_orders.py
│ └── test_products.py
└── src
├── env.example
├── poetry.lock
├── pyproject.toml
├── app.py
├── config
│ ├── base.py
│ └── __init__.py
├── assets
│ ├── images
│ └── documents
├── db
│ ├── db.py
│ └── __init__.py
├── dto
│ ├── __init__.py
│ ├── order_items.py
│ ├── orders.py
│ └── products.py
├── enums
│ ├── __init__.py
│ └── statuses.py
├── __init__.py
├── migrations
│ ├── env.py
│ ├── README
│ ├── script.py.mako
│ └── versions
│ └── c89a019488e0_.py
├── models
│ ├── __init__.py
│ ├── order_items.py
│ ├── orders.py
│ └── products.py
├── repositories
│ ├── __init__.py
│ ├── order_items.py
│ ├── orders.py
│ └── products.py
├── routers
│ ├── __init__.py
│ ├── orders.py
│ └── products.py
├── schemas
│ ├── __init__.py
│ ├── order_items.py
│ ├── orders.py
│ └── products.py
├── services
│ ├── __init__.py
│ ├── orders.py
│ └── products.py
└── utils
└── __init__.py
/ - В корне проекта у нас README.md, тесты, файлы конфигураций, различные инфраструктурные файлы на подобии .gitlab-ci.yaml и docker. Полный список файлов и папок зависит от проекта. Корень проекта не должен быть перегружен исходным кодом. Если какие то файлы связаны между собой их можно объединить в папку
/src - весь исходный код проекта
/src/models - SQLAlchemy модели. Каждая в своем модуле а не все в одном файле.
/src/routers - Не стоит хранить все endpoints API в одном файле. Создавайте отдельные модули. Не всегда, но как правило можно создать отдельный модуль для каждой модели.
/src/db - модули для работы с БД. Создание сессии и т.д.
/src/migrations - Alembic миграции
/src/repositories - слой для работы с БД
/src/dto - DTO как контракт по которому слои общаются между собой. Например репозитории не должны возвращать модель SQLAlchemy или словари из своих методов. Вместо этого они должны возвращать или принимать DTO например на основе dataclass. Тоже самое касается сервисов.
/src/services - слой сервисов
/src/schemas - Схемы валидации входящий данных по API в routes например Pydantic модели.
/src/utils - Различные общие модули, хелперы и т.д, если они у вас есть.
/src/assets/<тип_ресурса>/<ресурс> - различные ресурсы картинки, документы и т.д.
Остальные папки это объединение в группы (папки), каких то модулей используеых в разных слоях, например enums.
Соглашения об именовании
Не пренебрегайте тем, как вы именуете сущьности в своем ТЗ. Это касается всего у чего есть имя пакеты, модули, функции, переменные, таблицы в БД, индексы и т.д.
По Python следует руководствоваться PEP8 правилами там все подробно описано и использовать линтер. Линтер (например flake8) частично поможет автоматизировать соблюдение PEP8
Тоже самое касается БД. Если сомневаетесь как именовать что либо, то гуглите например PostgreSQL naming conventions. В сети полно примеров как принято именовать сущности в БД и т.д.
Работа с БД
Пожалуй, самая важная часть ТЗ для бэкенд-программиста это то, как вы работаете с БД. Все описанное ниже обязательно.
Создание сессии
При работе с транзакциями в БД необходимо использовать контекстный менеджер и избегать того, чтобы в коде у вас открывалась и закрывалась (session.commit()) транзакция "руками".
Пример реализации модуля работы с БД с контекстным менеджером
from contextlib import asynccontextmanager
from sqlalchemy.ext.asyncio import create_async_engine, async_sessionmaker
from sqlalchemy.orm import DeclarativeBase
from src.config.base import Config
class ModelBase(DeclarativeBase):
pass
engine = create_async_engine(Config.DATABASE_URL, echo=True)
Session = async_sessionmaker(bind=engine)
@asynccontextmanager
async def get_db_session():
session = Session()
try:
yield session
await session.commit()
except Exception as e:
await session.rollback()
raise e
finally:
await session.close()
Как использовать контекстный менеджер. Это просто пример. В реальности мы должны создавать сессию на уровень выше репозиториев, где у нас все запросы, и соблюдать dependency injection.
async with get_db_session() as db_session:
order = Order(**data)
db_session.add(order)
# Eсли необходимо полуить ID новой записи order не делая commit.
await db_session.flush()
Обратите внимание на эту строку из примера реализации модуля работы с БД выше:
engine = create_async_engine(Config.DATABASE_URL, echo=True)
С помощью флага echo=True мы включаем логирование всех запросов и можем видеть все запросы, которые генерирует ORM SqlAlchemy.
Если по ТЗ мы должны использовать ORM - этот флаг очень полезен, если у вас еще мало опыта работы с ORM. Так вы сможете легко отслеживать какие запросы реально выполняются в БД.
Query N + 1
Одна из самых распространенных ошибок. Часто вопрос об этом задают еще на этапе собеседования с HR.
Давайте рассмотрим пример
Тут содержатся неточности и код предназначен исключительно для иллюстрации.
class Order(ModelBase):
__tablename__ = 'orders'
id: Mapped[int] = mapped_column(
BigInteger,
primary_key=True,
autoincrement=True,
)
items = relationship('OrderItem')
# Получаем все заказы
# Тут будет выполнен примерно такой запрос
"""
SELECT * FROM orders;
"""
orders = session.query(Order).all()
# Получаем товары из каждого заказа
for order in orders:
# Здесь происходит отдельный запрос для каждого item в order.items
# Т.е. на каждой итерации цикла запрос в БД
"""
SELECT *
FROM order_items
WHERE order_items.order_id = <order_id>;
"""
for item in order.items:
# какой то код
Если у нас 10 товаров в заказе, то будет выполнено 1 + 10 = 11 запросов, что приводит к Query N+1.
Решение проблемы Query N+1
Чтобы избежать Query N+1, можно использовать жадную загрузку (eager loading) через joinedload или subqueryload.
Использование joinedload
from sqlalchemy.orm import joinedload
# Заранее загружаем связанные элементы в заказе
# тут будет выполнен запрос с JOIN
"""
SELECT *
FROM orders
LEFT OUTER JOIN order_items ON orders.id = order_items.order_id;
"""
orders = session.query(Order).options(joinedload(Order.items)).all()
for order in orders:
# Данные уже загружены с помощью joinedload
# дополнительных запросов нет
for item in order.items:
# какой то код
Индексы
Кто-то может подумать, что индексы - это преждевременная оптимизация, но я поспешу вам напомнить, что ТЗ это конкурсная работа, и ваша задача показать максимум ваших знаний.
Убедитесь, что в вашем решении во всех запросах к БД, где есть фильтрация по каким-то полям(условия в where), используются индексы и есть уникальные ограничения там, где это необходимо (например email пользователя и т.д.).
Пример с many-to-many связями
Таблица описывает связь заказа с товаром. Каждая запись этой таблицы должна содержать уникальную пару order_id и product_id. Не забывайте, что уникальное ограничение одновременно является уникальным индексом, и отдельно создавать индекс не нужно.
class OrderItem(ModelBase):
__tablename__ = 'order_items'
id: Mapped[int] = mapped_column(
BigInteger,
primary_key=True,
autoincrement=True
)
order_id: Mapped[int] = mapped_column(
ForeignKey('orders.id', ondelete='CASCADE'))
product_id: Mapped[Optional[int]] = mapped_column(
ForeignKey('products.id', ondelete='SET NULL'))
__table_args__ = (
UniqueConstraint(
'order_id',
'product_id',
name='ix_order_items_order_id_product_id'
),
)
Не создавайте отдельные индексы по каждому полю, если они используются в одном запросе. В такой ситуации используйте Multicolumn Indexes. Конечно это уже зависит от конкретной ситуации.
Обратная связь от работодателя
Не стоит рассчитывать на то, что вы научитесь делать ТЗ получая обратную связь от работодателей и исправляя ошибки.
Работодатель почти никогда не дает обратную связь о том, что было сделано не так. Чаще всего HR просто перестает отвечать, если ваше решение не подошло или просто не было проверено.
Так происходит потому что кандидатов на позицию Junior очень много и работодателю нецелесообразно тратить время на то, чтобы проверить все ТЗ и всем дать обратную связь.
Не воспринимайте это близко к сердцу просто готовьтесь к выполнению ТЗ заранее не надеясь на то, что код ревью вам будут делать работодатели.
Могу дать один важный совет, который сильно увеличивает шансы, что ваше ТЗ будет проверено.
Мониторьте вакансии и старайтесь откликаться на вакансию сразу, как она появилась. Если вакансия висит например месяц, то на нее уже не только откликнулось большое количество людей, но и многие уже выполнили ТЗ. В такой ситуации, даже если вы сделаете ТЗ идеально и лучше всех, шансы получить работу не высокие, так как ваша работа будет далеко не первая в очереди на проверку, и до нее могут даже не дойти.
Заключение
Тестовое задание — это конкурсная работа.
Помните, что качественное выполнение ТЗ начинается с базовых вещей: внимательно прочитайте задание, следуйте требованиям, покажите структурированный подход и профессионализм в каждой детали — от README.md до индексов в базе данных. Даже если вас не возьмут на эту позицию, вложенные усилия станут хорошей практикой и сделают вас лучше как разработчика.
Развивайте свои навыки, изучайте лучшие практики и анализируйте чужие успешные решения. Делая это, вы не только улучшите свои шансы на получение работы, но и заложите фундамент для успешной карьеры в разработке.
И самое главное — помните, что путь к мечте не всегда бывает легким. Продолжайте стараться, даже если путь кажется трудным. Удачи в покорении новых вершин!
Комментарии (15)
izibrizi2
28.01.2025 17:14А где же solid и ddd?
alexgreendev Автор
28.01.2025 17:14Спасибо за комментарий) Действительно стоит упомянуть SOLID и DDD. Плюсую)
Green21
28.01.2025 17:14Я бы обязательно еще упомянул про менеджер версий питона - какой нибудь pyenv, чтобы в случае чего можно было быстро и легко переключаться между версиями. Про какой нибудь Pipenv для быстрой настройки виртуального окружения и установки зависимостей (с помощью Pipfile).
Ну и насчет индексов - индексировать как раз нужно столбцы, участвующие в фильтрах (условиях where ... ), просто про них обычно и забывают. А уникальные поля и так будут проиндексированы по умолчанию.
alexgreendev Автор
28.01.2025 17:14Спасибо за коментарий) Про pyenv и все остальное я бы написал отдельную статью или может даже видео про настройку рабочего окружения. Не хочется перегружать статью. Про индексы тоже все верно сказали в статье это так же упоминается. Плюсую)
phx
28.01.2025 17:14А где в структуре должна располагаться папка с ресурсами?
alexgreendev Автор
28.01.2025 17:14Уточни пожалуйста о каких ресурсах речь? Если исходный код, то в примере структуры папка src находится в корне проекта.
Pubert
28.01.2025 17:14Могу предложить, что комментатор ввиду, например, картинки/видео и др. В python backend реально почти не сталкивался с этим) но из опыта разработки на Java и php могу сказать, что папка res/ обычно располагается в корне проекта, а ресурсы в ней разделены либо по типам (res/documents, res/images), либо по тематике (например, res/pages/contact, res/uploads/hdjdjehhd.pdf) в зависимости от приложения. Если это игра, то проще разделять по типам. Если проект большой - по тематике. Это из личного опыта, если кто не согласен, или в питоне другие правила - пишите, самому интересно
alexgreendev Автор
28.01.2025 17:14Спасибо) Согласен с комментарием выше. В примере структуры проекта из статьи ресурсы можно расположить в папке src/assets/<тип_ресурса>/<ресурс>
vanelm
28.01.2025 17:14Орфографию, всё же желательно проверять перед публикацией.
alexgreendev Автор
28.01.2025 17:14Согласен, спасибо) В ближайшее время пробегусь и все исправлю) В следующий раз не буду полагаться только на глаза)
pomponchik
Зачем писать зависимости в readme, если для этого есть requirements.txt, lock-файлы etc.?
alexgreendev Автор
Спасибо за комментарий) Я там указал что речь идет не о pip зависимостях и т.д., а об окружении, которое необходимо для запуска проекта. Конечно перечислять используемые библиотеки\пакеты скорее всего будет лишним. Так же этот пункт опционален, и каждый сам должен определить о чем необходимо написать, чтобы не было проблем с запуском.