Уровень 2 · Стили Глава 07 10 мин

Screaming Architecture

Структура папок должна кричать о бизнесе, а не о фреймворке. Package-by-feature vs package-by-layer. Vertical Slice Architecture (Jimmy Bogard).

TL;DR

  • Первое, что видит новый разработчик — структура папок. Она должна говорить о бизнесе, а не о фреймворке.
  • Package-by-feature (orders/, payments/) обычно лучше package-by-layer (controllers/, services/, repositories/).
  • Vertical Slice Architecture (Bogard) — крайняя форма: полный стек внутри одной feature-папки.
  • Комбинирование layered внутри feature — разумный компромисс для средних проектов.

Тест: что кричит ваш проект

Your architectures should scream about the use cases of the application, and not about the frameworks you used.

— Robert C. Martin Screaming Architecture, 2011

Откройте свой проект. Что видит новый разработчик в корне?

Вариант A:

app/
├── controllers/
├── models/
├── serializers/
├── services/
├── views/
└── urls/

Проект кричит: «я Django/Rails-приложение». Что оно делает? Непонятно — нужно открывать файлы.

Вариант B:

app/
├── orders/
├── payments/
├── inventory/
├── shipping/
└── customers/

Проект кричит: «я система обработки заказов». Framework вторичен.

Uncle Bob (Martin) считает, что вторая структура — правильная. И даже жёстче: если по структуре папок нельзя понять, что делает система — это архитектурный смелл.

Package-by-layer vs package-by-feature

Определение Package-by-layer

Структура, где верхнеуровневые пакеты соответствуют техническим слоям (controllers, services, repositories).

Определение Package-by-feature

Структура, где верхнеуровневые пакеты соответствуют бизнес-возможностям (orders, payments, shipping).

Разница на реальном примере:

Package-by-layer:

app/
├── controllers/
│   ├── order_controller.py
│   ├── payment_controller.py
│   └── shipping_controller.py
├── services/
│   ├── order_service.py
│   ├── payment_service.py
│   └── shipping_service.py
├── repositories/
│   ├── order_repository.py
│   ├── payment_repository.py
│   └── shipping_repository.py
└── models/
    ├── order.py
    ├── payment.py
    └── shipping.py

Package-by-feature:

app/
├── orders/
│   ├── controller.py
│   ├── use_cases.py
│   ├── repository.py
│   └── model.py
├── payments/
│   ├── controller.py
│   ├── use_cases.py
│   ├── repository.py
│   └── model.py
└── shipping/
    ├── controller.py
    ├── use_cases.py
    ├── repository.py
    └── model.py

Почему feature-first обычно лучше

    Изменения локализованы. Новая фича по заказам трогает одну папку orders/, а не пять разных папок.

    Соблюдение принципа Common Closure. «То, что меняется вместе, лежит вместе.» Классика (Martin).

    Легче удалять. Ушёл функционал shipping/ — удалили папку. В package-by-layer — придётся выковыривать из 5 разных папок.

    Явные границы модулей. Каждая feature-папка — потенциально bounded context. Легче потом резать на микросервисы.

    Meaningful diff в PR. Изменения по одной feature = список файлов в одной папке. Ревьюеру ясно.

Когда package-by-layer оправдан

Не всегда feature-first лучше:

  • Очень маленький проект — 3-5 файлов. Дробить на feature-папки = искусственное усложнение.
  • Строгое разделение команд по слоям — фронтендеры, бэкендеры, DBA. Редко и уже проблема сама по себе.
  • Много кросс-фичевого кода — если каждая фича трогает одни и те же таблицы, границы фич не проходят.

Vertical Slice Architecture

Jimmy Bogard (автор MediatR) довёл идею до логического конца в 2018-м:

Rather than have a set of technical layers, we organize code around business features. Each slice contains everything needed to fulfill that feature.

— Jimmy Bogard Vertical Slice Architecture, 2018
Определение Vertical Slice Architecture

Организация кода по сценариям (use case). Внутри слайса — полный стек: контроллер, use case, model, доступ к данным. Между слайсами — минимум общего кода.

Пример:

app/
├── orders/
│   ├── place_order/
│   │   ├── endpoint.py         # POST /orders
│   │   ├── command.py          # PlaceOrderCommand
│   │   ├── handler.py          # PlaceOrderHandler
│   │   ├── validator.py
│   │   └── response.py
│   ├── cancel_order/
│   │   ├── endpoint.py
│   │   ├── command.py
│   │   ├── handler.py
│   │   └── response.py
│   └── shared/
│       ├── models.py           # Order aggregate
│       └── repository.py
└── shipping/
    ├── ship_order/
    │   └── ...
    └── track_shipment/
        └── ...

Каждый use case — своя папка. Никакого «Order Service» с 15 методами. Одна папка = один сценарий.

Bogard не претендует на универсальность. Но для CQRS-стеков и MediatR-подобных решений — Vertical Slice оптимален.

Комбинация: слои внутри features

Практический компромисс:

app/
├── orders/                    # Bounded context
│   ├── domain/                # entities, VO, aggregates
│   ├── application/           # use cases + ports
│   ├── infrastructure/        # repositories, adapters
│   └── interface/             # HTTP handlers
├── payments/
│   ├── domain/
│   ├── application/
│   ├── infrastructure/
│   └── interface/
└── shared/
    ├── kernel/               # общие Value Objects (Money, DateRange)
    └── platform/             # логгер, метрики, DI

Верхний уровень — feature-first (screaming). Внутри — layer-first (Hexagonal/Clean).

Плюсы:

  • Границы модулей (bounded contexts) явные.
  • Внутри модуля — привычная Hexagonal-структура.
  • shared/ минимален — только общие примитивы.

Этот подход — де-факто стандарт для модульных монолитов.

Как не надо

1. «Utils» на верхнем уровне

app/
├── orders/
├── payments/
├── utils/               # !!
│   ├── helpers.py
│   ├── decorators.py
│   └── constants.py

utils/ — свалка. Со временем туда попадает всё, что «не подошло» в feature-папку. К году там 200 файлов, из которых ни одна feature не может отказаться, потому что все от них зависят.

Правильно: конкретный helper живёт рядом с использующим кодом. Общие примитивы (Money, DateRange) — в shared/kernel/.

2. Тонкие feature-папки

app/
└── orders/
    ├── controller.py         # 5 строк
    ├── service.py            # 500 строк (весь код)
    └── repository.py         # 20 строк

Feature-first, но внутри — та же процедурная layered. Ничего не выиграли.

Проблема: orders/service.py — жирный сервис. См. главу Layered — там разбирали.

3. Feature-папка ≠ bounded context

app/
├── users/
│   ├── auth.py
│   ├── profile.py
│   ├── notifications.py
│   └── billing.py

users/ — это не feature. Это domain «пользователи», в котором сидят auth, profile, notifications, billing — четыре разных контекста. Всё, что связано со словом «user», сброшено в одну папку.

Правильно: разделить на auth/, profile/, notifications/, billing/. Каждый — bounded context. Внутри — своя модель User (Auth-User ≠ Billing-User).

4. Framework в имени папки

app/
├── flask_routes/
├── celery_tasks/
├── sqlalchemy_models/

Проект кричит не «я система заказов», а «я использую Flask + Celery + SQLAlchemy». Это анти-Screaming Architecture. Смена фреймворка = ломание структуры.

Правильно: имена по бизнес-функциям, framework — infrastructure detail.

Тесты Screaming Architecture

Три быстрых теста для существующего проекта:

    Тест закрытой книги. Закройте README.md, откройте структуру папок. Понятно ли, чем система занимается? Если нужны файлы — Screaming не соблюдён.

    Тест удаления фичи. Удалите одну бизнес-фичу. Сколько папок и файлов затронуто? Больше 3 в разных пакетах — фичи размазаны.

    Тест grep по фреймворку. grep -r "flask\|django\|fastapi" app/domain/. Ноль совпадений — хорошо. Ненулевые — фреймворк проник в бизнес-слой.

Trade-offs

СитуацияPackage-by-featurePackage-by-layer
Проект > 20 файлов✓
Планируется дробить на микросервисы✓
Команда > 3 человек✓
Многие изменения затрагивают одну feature✓
Проект — 5 файлов «hello world»✓ (не критично)
Строгое разделение по слоям в команде✓ (редко оправдано)

Правило большого пальца: если проект живёт > 3 месяцев или команда > 2 человек — feature-first.

В твоём же коде

Анонимизированный микросервис использует package-by-layer:

app/
├── application/
├── domain/
├── infrastructure/
├── core/
└── interface/

Для сервиса с одним use case это работает — «feature» и «сервис» совпадают. Проект кричит: «я сервис-оркестратор с чётким Hexagonal».

Если бы сервис разросся до 5 use case’ов (нормализация + reprocess + retry + audit + export), лучше стало бы:

app/
├── standardize/       # feature
│   ├── domain/
│   ├── application/
│   └── infrastructure/
├── reprocess/
├── audit/
├── export/
└── shared/
    └── kernel/       # общие Value Objects

Границы фич стали бы явными, и каждую можно было бы обсуждать отдельно.

Дальнейшее чтение

  • Robert C. Martin. Screaming Architecture. 2011. Оригинальный пост.
  • Robert C. Martin. Clean Architecture. Chapter 21 — Screaming Architecture целиком.
  • Jimmy Bogard. Vertical Slice Architecture. 2018. Feature-first доведён до предела.
  • Simon Brown. Modular Monoliths. Как feature-first ведёт к модульному монолиту.
  • Herberto Graça. Screaming Architecture. Хороший разбор с картинками.

Проверьте себя

Мини-quiz · закрепить

Проверьте себя

  1. Q1. Что должна «кричать» структура папок правильно спроектированного приложения?

  2. Q2. Когда package-by-layer предпочтительнее package-by-feature?

  3. Q3. Vertical Slice Architecture отличается от простого package-by-feature тем, что...

  4. Q4. Какие проблемы у Vertical Slice Architecture?

  5. Q5. Тест «закрытой книги» (для проверки Screaming Architecture) — это...