Чистая архитектура в Python
Когда сервис перерастает один файл, встаёт вопрос, как разложить код, чтобы бизнес-логику можно было тестировать и менять фреймворк, не переписывая всё. Собес на middle+ проверяет, понимаешь ли ты слои и направление зависимостей, и, что важнее, чувствуешь ли, где это оверинжиниринг.
Типовые формулировки: «зачем репозиторий и сервисный слой?», «что такое инверсия зависимостей на практике?», «когда слои - перебор».
Слои и направление зависимостей
Идея одна: домен независим от фреймворка и БД, а зависимости направлены ВНУТРЬ - инфраструктура знает о домене, но не наоборот. Тогда можно сменить FastAPI на что угодно или БД на другую, не трогая ядро бизнес-правил.
Инструменты. Репозиторий прячет доступ к данным за интерфейсом - логика не знает про SQL или конкретный ORM. Сервисный слой держит бизнес-сценарии вне HTTP, поэтому эндпоинт становится тонким: распарсил запрос, вызвал сервис, вернул ответ. DTO (data transfer object) (схемы обмена) развязывают внешний контракт API и внутреннюю доменную модель, чтобы изменение одного не ломало другое.
// Импортировать FastAPI в доменном слое - значит привязать ядро к транспорту; ровно то, от чего слои и защищают.
- репозиторий
- абстракция доступа к данным над ORM/SQL
- сервисный слой
- бизнес-сценарии вне HTTP-обработчиков
Инверсия зависимостей и её цена
Инверсия зависимостей: логика зависит не от конкретной реализации, а от абстракции - Protocol или ABC (abstract base class), а саму реализацию внедряют извне. Тогда в тесте вместо настоящего репозитория с БД подставляешь заглушку в памяти, и бизнес-логика тестируется без базы, быстро и изолированно. Это главный практический выигрыш слоёв - тестируемость.
Но у слоёв есть цена - церемонии. На простом CRUD полный набор репозиториев, сервисов и DTO избыточен: пишешь три файла там, где хватило бы одного эндпоинта. Слои вводят ПО МЕРЕ роста сложности, а не с первой строки, иначе платишь скоростью и читаемостью за архитектуру, которая пока не нужна.
// Признак зрелости на собесе - назвать не только плюсы слоёв, но и честно сказать, где они лишние.
- инверсия зависимостей
- зависеть от интерфейса, реализацию внедрять
- DTO
- схема обмена, отдельная от доменной модели
Как отвечать: «Зачем репозиторий, если можно вызвать ORM прямо в эндпоинте?»
Ради тестируемости и свободы менять инфраструктуру. Репозиторий прячет доступ к данным за интерфейсом, поэтому бизнес-логика не знает, SQL там, ORM или вообще внешний сервис, а в тесте я подменяю его заглушкой в памяти и проверяю логику без поднятой БД, быстро. Плюс если ORM или схема поменяются, правка локализована в репозитории, а не размазана по эндпоинтам. Но я честно смотрю на сложность: на простом CRUD это оверинжиниринг, там ORM прямо во вью нормально. Слои я ввожу, когда логика перерастает тонкий обработчик.
Названа главная выгода (тест без БД через подмену), добавлена локализация изменений и - ключевое для зрелости - оговорка про оверинжиниринг на CRUD; это баланс, а не догма.
На чём валят
- −Держать бизнес-логику в теле эндпоинта: её не переиспользовать и тяжело тестировать без веба.
- −Импортировать фреймворк (FastAPI) в доменном слое - привязывает ядро к транспорту.
- −Разворачивать все слои на простом CRUD: оверинжиниринг ценой скорости и читаемости.
- −Путать DTO и доменную модель: тащить схему API внутрь домена, и внешний контракт начинает диктовать логику.
Проверьте себя
Пять вопросов из банка по этой подтеме. Всего их 15, остальные разбираются в тренажёре.
- Основная идея слоистой (чистой) архитектуры приложения — какая?A)Разложить весь код по как можно большему числу папок для внешней аккуратностиB)Собрать всю логику в одном большом модуле, чтобы ничего не искать по проектуC)Убрать слой базы данных полностью, храня состояние приложения только в памятиD)Отделить бизнес-логику от фреймворка и БД, чтобы менять их независимо
показать ответ и разбор
+D)Отделить бизнес-логику от фреймворка и БД, чтобы менять их независимо// разбор: Слоистая архитектура держит бизнес-правила (домен) в центре, независимыми от деталей: веб-фреймворка, БД, внешних API. Зависимости направлены внутрь — инфраструктура знает о домене, а не наоборот. Тогда FastAPI можно заменить, БД — сменить, а ядро логики останется нетронутым и тестируемым без всего этого окружения.
- Что даёт паттерн «репозиторий»?A)Прячет доступ к данным за интерфейсом — логика не знает про SQL и ORMB)Ускоряет запросы к базе данных, автоматически добавляя кеш поверх каждого обращенияC)Генерирует классы моделей автоматически на основе структуры существующих таблицD)Заменяет базу данных, храня все объекты предметной области прямо в оперативной памяти
показать ответ и разбор
+A)Прячет доступ к данным за интерфейсом — логика не знает про SQL и ORM// разбор: Репозиторий — объект-коллекция для доменных сущностей: get, add, list, delete — за этим интерфейсом скрыто, что внутри SQLAlchemy, сырой SQL, внешний API или словарь в памяти. Бизнес-логика работает с абстракцией, не зная деталей хранения. Это упрощает тесты (подставить in-memory репозиторий) и смену хранилища.
- Зачем выделять сервисный слой (use cases) между эндпоинтами и данными?A)Собрать бизнес-логику отдельно от HTTP — переиспользуема и тестируема без вебаB)Для автоматической генерации документации OpenAPI по бизнес-операциям приложенияC)Чтобы напрямую вызывать SQL из обработчиков, минуя все промежуточные слоиD)Чтобы эндпоинты стали длиннее и подробнее описывали каждый шаг обработки запроса
показать ответ и разбор
+A)Собрать бизнес-логику отдельно от HTTP — переиспользуема и тестируема без веба// разбор: Сервисный слой держит сценарии предметной области (оформить заказ, начислить бонус) отдельно от HTTP-обработчиков. Эндпоинт тогда тонкий: распарсил вход, вызвал сервис, вернул результат. Логику можно переиспользовать (из CLI, воркера, другого эндпоинта) и тестировать без поднятия веб-стека. Толстые обработчики с логикой внутри — то, чего избегают.
- Принцип инверсии зависимостей: как его выражают в Python-коде?A)Логика зависит от абстракции (Protocol/ABC), а конкретную реализацию внедряют извнеB)Каждый модуль сам импортирует и создаёт нужные ему конкретные классы прямо внутри себяC)Порядок импортов в файлах выстраивают строго от низких модулей к высокимD)Все зависимости объявляют глобальными переменными на уровне пакета приложения
показать ответ и разбор
+A)Логика зависит от абстракции (Protocol/ABC), а конкретную реализацию внедряют извне// разбор: Инверсия зависимостей (буква D в SOLID): высокоуровневая логика не зависит от низкоуровневых деталей — обе зависят от абстракции. В Python абстракцию задают Protocol или ABC (интерфейс репозитория, шлюза), сервис принимает её в конструкторе, а конкретную реализацию (SQLAlchemy-репозиторий) подставляют при сборке. Ядро не импортирует инфраструктуру.
- Зачем разделять доменную модель и DTO (схему запроса/ответа)?A)Внешний контракт API не диктует внутреннюю модель — их можно менять раздельноB)DTO работают быстрее доменных объектов, поэтому их используют вместо моделей вездеC)DTO нужны только для базы данных, а доменная модель — только для веб-слояD)Это просто дублирование ради формальности, никакой практической пользы в нём нет
показать ответ и разбор
+A)Внешний контракт API не диктует внутреннюю модель — их можно менять раздельно// разбор: DTO (Pydantic-схемы на входе/выходе) описывают контракт с внешним миром, доменная модель — бизнес-сущность с правилами. Разделив их, вы меняете форму API, не трогая логику, и наоборот; не утекают внутренние поля; валидация транспорта отделена от инвариантов домена. Цена — маппинг между ними, оправданный в нетривиальных системах.
дальше
Теорию прочитали. Навык ставится повторением
В Сеньорчике эта подтема идёт в ежедневных сессиях: движок возвращает её, пока ответы не станут уверенными, и ведёт прогресс отдельно по каждой подтеме. Теория внутри тоже бесплатна, лимит только на количество вопросов в день.