Прямой ответ
Классическая слоистая структура группирует код по технической роли: все контроллеры в одном каталоге, все модели в другом, все сервисы в третьем. Вертикальный срез группирует по сценарию: всё, что нужно для оформления заказа, лежит в одном месте.
# Слоистая структура # Вертикальные срезы
src/ src/
controllers/ orders/
orders.py create_order.py
payments.py cancel_order.py
services/ schemas.py
order_service.py repository.py
payment_service.py payments/
repositories/ charge_payment.py
order_repo.py schemas.py
models/ repository.py
order.py shared/
payment.py db.py
errors.py
Практическое отличие проявляется при изменении. Добавление поля в сценарий оформления заказа при вертикальных срезах затрагивает один каталог; при слоистой структуре — четыре каталога и четыре файла, между которыми нужно удерживать соответствие.
Файл сценария содержит весь путь обработки:
# src/orders/create_order.py
from pydantic import BaseModel
from shared.db import Session
class CreateOrderRequest(BaseModel):
customer_id: str
items: list[OrderItemIn]
class CreateOrderResponse(BaseModel):
order_id: str
total: Decimal
async def create_order(payload: CreateOrderRequest, db: Session) -> CreateOrderResponse:
order = Order(customer_id=payload.customer_id)
for item in payload.items:
order.add_item(item.sku, item.qty, item.price)
db.add(order)
await db.commit()
return CreateOrderResponse(order_id=order.id, total=order.total())
Запрос, валидация, логика и сохранение видны в одном файле. Это не отменяет разделения ответственности — оно остаётся внутри сценария, но не требует перехода между каталогами.
Важное следствие: вертикальные срезы работают только при контроле зависимостей. Без правила «сценарий не импортирует внутренности другого сценария» структура вырождается в набор файлов без границ. Контроль задаётся анализатором импортов; о настройке — в статье про структуру проекта.
Практика: когда применять и как поддерживать
1. Оставляйте общий слой тонким. В shared попадают подключение к базе, ошибки, утилиты. Как только там появляется бизнес-логика, она перестаёт быть общей и превращается в свалку.
2. Запретите перекрёстные импорты сценариев. Один сценарий обращается к другому через его публичный интерфейс, а не через внутренние функции. Проверка выполняется в CI.
3. Не дробите мелкие сценарии. Сценарий из одного запроса не требует пяти файлов; достаточно одного. Дробление ради симметрии усложняет чтение.
4. Выделяйте домен, когда правила усложняются. Если бизнес-правила сценария разрослись, их выносят в отдельный модуль предметной области, оставляя в сценарии только обработку запроса.
5. Совмещайте с модульным монолитом. Срезы — способ организации внутри модуля; границы модулей задаются отдельно — см. статью про модульный монолит.
6. Следите за дублированием. Один и тот же код в трёх сценариях — сигнал, что его место в общем модуле, а не копирование.
7. Проверяйте структуру тестами. Тест сценария лежит рядом с ним и проверяет поведение целиком, включая валидацию и сохранение; о пирамиде — в статье про тестовую пирамиду.
8. Разделяйте сценарий и доменную логику. Если правила расчёта скидки занимают больше кода, чем обработка запроса, их место в доменном модуле, а не в файле сценария. Срез отвечает за путь запроса: принять, проверить права, вызвать домен, сохранить, отдать ответ.
9. Держите маршруты ближе к сценарию. Регистрация маршрута рядом с обработчиком делает связь очевидной; отдельный файл со всеми маршрутами возвращает структуру к слоистому подходу.
10. Не создавайте срез для каждой мелочи. Чтение справочника из одной таблицы не требует отдельного каталога. Избыточное дробление увеличивает число файлов без роста ясности.
Рекомендации:
- Переходите постепенно. Начните с новых сценариев, а не с переписывания существующей структуры.
- Фиксируйте правила в репозитории. Конфигурация анализатора импортов делает договорённость проверяемой.
- Не смешивайте два подхода в одном модуле. Половина кода по слоям и половина по сценариям хуже любого из вариантов.
- Именование по действию. Имя файла описывает сценарий:
create_order.py, а неorder_service.py. - Учитывайте фреймворк. Механизм зависимостей фреймворка упрощает срезы; о приёмах — в статье про зависимости FastAPI.
Практический сценарий: изменение одного поля требовало правки шести файлов
Контекст: в проект добавили поле «желаемая дата доставки» в сценарий оформления заказа. Правки затронули шесть файлов в четырёх каталогах.
- Диагностика. Изменение прошло через файл запроса, схему DTO, сервис, репозиторий, модель и тесты в отдельном каталоге. Половина правок была механической: проброс поля между слоями.
- Проверка истории. Анализ двадцати последних задач показал, что 14 из них имели тот же профиль: изменение одного сценария требовало правок в трёх-шести файлах в разных каталогах.
- Действия. (а) Новые сценарии начали создавать как вертикальные срезы; (б) ввели контракт в анализаторе импортов, запрещающий обращение к внутренностям чужого сценария; (в) вынесли подключение к базе и доменные ошибки в
shared; (г) существующие сценарии переводили постепенно, начиная с тех, которые менялись чаще; (д) тесты перенесли рядом со сценариями. - Результат. Через три месяца медианное число файлов на изменение сценария сократилось с четырёх до двух; правки стали локализованы, и число конфликтов слияния в общих файлах снизилось.
Вывод: слоистая структура оплачивается пробросом данных между слоями при каждом изменении. Вертикальные срезы убирают эту стоимость, но требуют контроля зависимостей, иначе границы размываются быстрее, чем в слоистом варианте.
FAQ
Подходят ли вертикальные срезы для больших проектов? Да, при контроле зависимостей. Основной риск — перекрёстные импорты, которые превращают срезы в один большой модуль.
Чем срез отличается от модуля? Срез описывает сценарий использования, модуль — область предметной области. Внутри модуля может быть несколько срезов.
Нужно ли отказываться от слоёв полностью? Нет. Разделение на запрос, логику и сохранение остаётся внутри сценария; убирается только физическое разнесение по каталогам.
Что делать, если сценарий разросся до тысячи строк? Это признак того, что внутри него несколько сценариев либо доменная логика не вынесена. Сначала разделяют сценарии, затем выносят правила в домен; разбивать один сценарий на слои не нужно.
Как быть с общими моделями данных? Модель предметной области выносят в доменный модуль, общий для нескольких сценариев. Дублирование модели в каждом срезе — ошибка.
Источники: Martin Fowler (PresentationDomainDataLayering), Python docs, import-linter — ссылки в шапке.