Перейти к основному содержимому

Организация структуры кодовой базы

Разработчику Архитектору Инженеру
Связанные материалы

Процесс разработки и сдачи кода — Процесс разработки ПО.

Оформление репозитория — README для разработчика.

Архитектура solution на .NET — Clean Architecture на ASP.NET Core.

Паттерны и слои в проектировании — раздел 7.06 "Проектирование и архитектура".


Структура кодовой базы в "чистой архитектуре"

Структура кодовой базы - навигация и границы модулей

Отличный пример структуры папок — это проявление слоистой архитектуры с элементами hexagonal (ports & adapters) и domain-driven Проектирование.

Сначала без снобизма, на пальцах — суть в том, чтобы разложить код по папкам так, чтобы бизнес-логика не зависела от базы данных, API или фреймворков.

На пальцах разложим роли (в коде им часто соответствуют папки):

РольСмыслПример
СущностьТо, что "есть" в предметной области, с правиламиOrder, User — заказ нельзя оплатить дважды
Действие (use-case)Что система делает по запросу"Оформить заказ", "Зарегистрировать пользователя"
Обещание (port)"Мне нужен способ сохранить", без SQL внутриIOrderRepository.Save — интерфейс, без PostgreSQL
ИнфраструктураКак именно выполняется обещаниеEF Core, HttpClient, запись в файл
СервисыОбщая техника без бизнес-смыслахэш пароля, генерация GUID
DTOКонтейнер для JSON/API, без инвариантовOrderDto для ответа REST
События / handlersРеакция на фактпосле OrderPlaced — письмо на склад
ДекораторыЛог, кэш, транзакция вокруг use-caseне внутри каждой строки бизнес-кода
Свои ошибки"Заказ уже оплачен", а не голый Exceptionявная обработка на API
shared / utilsВсё прочее — по минимумуиначе превращается в свалку без границ

Определяем (кратко, как вы формулировали изначально):

  • сущность - то, что у нас есть (заказ, товар, пользователь);
  • что мы можем сделать (оплатить заказ, зарегистрировать юзера);
  • обещания - "мне нужен способ сохранить это в БД", без реализации;
  • реальную инфраструктуру - БД, API, файлы на диске;
  • всякие общие сервисы и задачи (хэширование, генерация токенов);
  • простые контейнеры для передачи данных наружу (пример - JSON);
  • обработчики и события - что делать, когда что-то случилось;
  • декораторы - обёртки для логов, кеша, проверок (чтобы не засорять бизнес-код);
  • свои типы ошибок;
  • и всё остальное - свалка-мусорка (держите utils под контролем, см. таблицу выше).

Это всё нужно для того, чтобы через месяц или на другом проекте мы могли заменить PostgreSQL на MongoDB, не переписывая бизнес-логику. Просто поменяем папку infrastructure. Тесты тоже станут вменяемыми.

НО! Если наш проект - три CRUD-таблицы и админка, эта структура только разозлит. Используйте для реально сложных бизнес-правил.

Ниже — назначение типичных директорий:

Play ITЗагрузка интерактивного демо…

Play ITЗагрузка интерактивного демо…

ДиректорияНазначениеКомментарий
entity, value-objectsЯдро предметной области. Идентичность и инварианты бизнес-логики.Должны быть свободны от зависимостей (чистые классы).
use-КейсыОркестрация операций над сущностями. Реализуют бизнес-транзакции.Зависят от entity, но не от инфраструктуры.
interface / portsАбстракции (интерфейсы) для внешних зависимостей (БД, API, FS).Определяют что, но не как.
infrastructureРеализации интерфейсов из interface.Содержат SqlConnection, HttpClient, File.WriteAllText и т.п.
servicesОбщая логика, не привязанная напрямую к use-case (например, хэширование, валидация).Может быть переиспользована.
dtosData Transfer Objects — для сериализации и межслойного обмена.Должны быть простыми контейнерами данных.
handlers, events, event-dispatcherРеализация событийной модели (in-process).Часто — mediator или observer pattern.
decoratorsCross-cutting concerns (логгирование, кэширование, валидация) через декораторы.Соответствует принципу Open/Closed.
filters, interceptorsОбработка запросов/ответов на уровне фреймворка (например, в ASP.NET Core).Не должны содержать бизнес-логику.
errorsСемантические типы ошибок (не Exception напрямую).Позволяют явно обрабатывать failure modes.
shared, utils, constants, typesВспомогательные элементы.Следует минимизировать — избыток utils признак низкой связанности модели.

Такая структура упрощает модульное тестирование, заменяемость компонентов и поддержку. Однако она оправдана при достаточной сложности предметной области; для простых CRUD-приложений — избыточна.


Зачем разделять папки, а не только "слои в голове"

Структура каталогов — это контракт для команды. Когда новый разработчик открывает репозиторий, он за минуты понимает:

  • где искать бизнес-правила (ядро, без SQL и HTTP);
  • где лежат контракты к внешнему миру (ports / interface);
  • где "грязные" детали (БД, почта, файлы, сторонние API).

Если всё свалено в Controllers/ и Services/, через полгода появляется "божественный сервис" на три тысячи строк, который знает и заказы, и Entity Framework, и отправку писем. Папки не магия — они ограничивают направление зависимостей: код из entity не должен импортировать infrastructure.

Стрелки зависимостей идут внутрь, к домену. Инфраструктура реализует порты, а не наоборот.


Пример дерева каталогов (учебный backend)

Ниже — упрощённый каркас сервиса заказов на .NET или любом ООП-стеке. Имена папок могут отличаться (Application, Domain, Infrastructure), смысл тот же:

src/
├── OrderService.Domain/ # entity, value-objects, errors
│ ├── Entities/
│ │ └── Order.cs
│ ├── ValueObjects/
│ │ └── Money.cs
│ └── Errors/
│ └── OrderErrors.cs
├── OrderService.Application/ # use-cases, ports (интерфейсы)
│ ├── UseCases/
│ │ └── PlaceOrder/
│ │ ├── PlaceOrderCommand.cs
│ │ └── PlaceOrderHandler.cs
│ └── Ports/
│ └── IOrderRepository.cs
├── OrderService.Infrastructure/ # реализации портов
│ ├── Persistence/
│ │ └── EfOrderRepository.cs
│ └── Email/
│ └── SmtpNotifier.cs
└── OrderService.Api/ # HTTP, DTO, filters
├── Controllers/
├── Dtos/
└── Program.cs

Правило импорта: ApiApplicationDomain; InfrastructureApplication + Domain. Domain ни от кого не зависит.


Сопоставление папки → ответственность на примере "Оформить заказ"

ШагГде в кодеЧто происходит
HTTP POST /ordersApiПринимает JSON, маппит в DTO
Валидация входаApi или ApplicationПроверка формата, не бизнес-инварианты
Сценарий "оформить"UseCases/PlaceOrderОркестрация: загрузить клиента, создать Order, сохранить
Инвариант "сумма > 0"entity/OrderМетод Place() бросает доменную ошибку
СохранениеPorts/IOrderRepositoryКонтракт "сохранить заказ"
Запись в PostgreSQLInfrastructure/EfOrderRepositoryEF, SQL, миграции — только здесь

Тест use-case без БД: подставляете фейковый IOrderRepository в памяти. Тест домена — вообще без фреймворка, только классы Order и Money.


Простой CRUD и "чистая" структура — когда что уместно

СитуацияРазумный минимумПолная слоистая + hexagonal
Админка на 3 таблицы, один разработчикControllers + Models + DataИзбыточно
Внутренний скрипт, живёт 2 месяцаОдин файл или пара модулейИзбыточно
Финтех, сложные статусы заказа, аудит, смена БДХотя бы Domain + InfrastructureОправдано
Несколько команд, долгая поддержка (5+ лет)Слои + явные границы модулейОправдано

Признаки, что пора усложнять структуру:

  • в контроллере появляется SQL или прямой вызов HttpClient;
  • один и тот же if про скидки копируется в трёх сервисах;
  • юнит-тесты требуют поднятия всей БД;
  • смена PostgreSQL на другую СУБД превращается в переписывание половины проекта.

DTO, entity и "модели для API" — не путать

ТипРольГде лежит
EntityБизнес-объект с инвариантамиentity / Domain
DTOКонтракт наружу (JSON, CSV)dtos, рядом с API
Persistence modelКак строка лежит в таблицечасто Infrastructure, иногда отдельный Persistence/Models

Одна сущность "Заказ" в жизни проекта может иметь три представленияOrder (домен), OrderDto (ответ API), OrderRow (таблица). Смешивать их в один класс "для удобства" — типичный источник багов при изменении API или схемы БД.


События, декораторы и cross-cutting

  • handlers / events — реакция на факт "заказ оформлен": отправить письмо, обновить склад. Домен публикует событие; подписчики живут в Application или Infrastructure, но не внутри Order.Place() как десять вызовов подряд.
  • decorators — обёртка вокруг use-case — логирование, метрики, транзакция. Бизнес-код остаётся коротким.
  • filters / interceptors — уровень HTTP (ASP.NET, Spring): аутентификация, correlation id. Без расчёта скидок и проверки остатков на складе.

Подробнее про медиатор и pipeline в .NET — MediatR в Clean Architecture.


Пошаговое введение слоёв в существующий проект

Если репозиторий уже "плоский", не переписывайте всё за неделю:

  1. Выделите один use-case (например, создание заказа).
  2. Вынесите правила в класс Order без зависимостей от EF.
  3. Введите интерфейс репозитория и перенесите SQL/EF в Infrastructure.
  4. Оставьте контроллер тонким: DTO → команда → handler → DTO ответа.
  5. Повторите для следующего сценария; общие utils подчищайте по мере дублирования.

Так структура растёт вместе с задачей, а не как "архитектурный Big Bang".


Чек-лист самопроверки структуры

  • Папка с доменом не ссылается на NuGet пакеты БД, веб-сервера, UI.
  • Use-case не содержит строк SQL и URL внешних API.
  • Для каждого порта есть ровно одна "боевая" реализация в infrastructure (плюс фейки в тестах).
  • DTO не утекли в домен (домен не знает про [JsonPropertyName]).
  • shared/utils не разросся до второго приложения — иначе выделите модуль осознанно.

Пет-проект с полным циклом (API + БД + деплой) помогает отработать слои на практике — см. Пет-проекты и План развития разработчика.