Уровень 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) — это...