Ответ
Доменный слой (Domain Layer) — это центральный, наиболее важный слой в архитектурах, основанных на предметной области (например, Domain-Driven Design, Чистая Архитектура). Он инкапсулирует ключевую бизнес-логику, правила и сущности приложения, полностью абстрагируясь от деталей инфраструктуры (база данных, UI, фреймворки) и внешних сервисов.
Его основная цель: Чёткое выражение бизнес-концепций и правил в коде, чтобы они были понятны как разработчикам, так и экспертам предметной области.
Ключевые строительные блоки доменного слоя:
- Сущности (Entities): Объекты, имеющие уникальную идентичность, которая сохраняется на протяжении всего жизненного цикла (например,
Customer,Order,Invoice). Они содержат как данные, так и поведение (методы, инкапсулирующие бизнес-правила). - Объекты-значения (Value Objects): Неизменяемые объекты, не имеющие идентификатора и определяемые исключительно своими атрибутами (например,
Address,Money,Color). Два Value Object с одинаковыми значениями считаются равными. - Агрегаты (Aggregates): Кластеры связанных сущностей и объектов-значений, которые рассматриваются как единое целое для операций изменения данных. У агрегата есть корень (Aggregate Root) — единственная сущность, через которую происходит всё внешнее взаимодействие с агрегатом.
- Доменные сервисы (Domain Services): Классы, содержащие бизнес-логику, которая по своей природе не принадлежит ни одной конкретной сущности или объекту-значению (например, сложный расчёт, затрагивающий несколько агрегатов).
- Репозитории (Repository Interfaces): Абстракции (интерфейсы), определяющие контракты для доступа к агрегатам. Их реализация находится в слое инфраструктуры.
- События домена (Domain Events): Способ информирования других частей системы о значимых изменениях, произошедших внутри домена.
Пример сущности (Aggregate Root) на C#:
public class Order : Entity<int> // Entity с Id типа int
{
// Внутреннее состояние. Коллекция защищена от прямого изменения.
private readonly List<OrderLine> _lines = new();
public CustomerId CustomerId { get; private set; } // Value Object для Id
public OrderStatus Status { get; private set; }
public IReadOnlyCollection<OrderLine> Lines => _lines.AsReadOnly();
// Публичные методы — это единственный способ изменить состояние.
// Они инкапсулируют бизнес-правила.
public void AddItem(Product product, int quantity)
{
// Правило 1: Нельзя добавлять товары в завершённый заказ.
if (Status == OrderStatus.Completed || Status == OrderStatus.Cancelled)
throw new InvalidOrderOperationException("Cannot modify a completed/cancelled order.");
// Правило 2: Количество должно быть положительным.
if (quantity <= 0)
throw new ArgumentException("Quantity must be greater than zero.", nameof(quantity));
// Правило 3: Если товар уже есть в заказе, увеличиваем количество.
var existingLine = _lines.FirstOrDefault(l => l.ProductId == product.Id);
if (existingLine != null)
{
existingLine.IncreaseQuantity(quantity);
}
else
{
_lines.Add(new OrderLine(product.Id, product.Price, quantity));
}
}
public void MarkAsCompleted()
{
// Правило: Нельзя завершить пустой заказ.
if (!_lines.Any())
throw new InvalidOrderOperationException("Cannot complete an empty order.");
Status = OrderStatus.Completed;
// Может генерировать Domain Event: new OrderCompletedEvent(this.Id)
}
}
Преимущества выделенного доменного слоя:
- Тестируемость: Логику можно тестировать изолированно, без БД или веб-сервера.
- Сохраняемость: Бизнес-правила не "растворяются" в коде инфраструктуры.
- Гибкость: Слой инфраструктуры (способ хранения данных) можно заменить, не затрагивая ядро бизнес-логики.
- Ясность: Код напрямую отражает бизнес-концепции, упрощая коммуникацию в команде.
Ответ 18+ 🔞
Давай разжую тебе эту тему, как будто объясняю за бутылкой пива после работы, а не на очередном скучном митапе про архитектуру.
Смотри, доменный слой — это, по сути, святая святых твоего приложения. Его мозги, его характер, его внутренние тараканы и все бизнес-правила, от которых у заказчика дергается глаз. Это то, ради чего всё и затевается. Не база данных, не красивый интерфейс, а вот эта самая логика: как считать деньги, как формировать заказ, когда можно отменить, а когда уже поздно. Если представить приложение как человека, то домен — это его личность, а всё остальное (база, апишки, кнопочки) — это просто одежда, руки-ноги и рот, чтобы эту личность выражать.
Зачем его выделять в отдельный слой? Да чтобы не превратить проект в одно большое спагетти, где логика "кликнули — сохранилось в базу" размазана по двадцати файлам, и через полгода никто не понимает, почему скидка применяется два раза. Ты же не хочешь, чтобы твой код через год вызывал только один вопрос: "Какого хуя?".
Из чего этот слой состоит? Да из понятных штук:
- Сущности (Entities): Это главные герои.
Заказ,Пользователь,Счёт. У них есть ID, они живут долго, меняются, но остаются собой. Они не просто мешки с данными — они умеют что-то делать.Заказ.ДобавитьТовар()— и внутри этого метода сидит проверка, можно ли вообще это сделать. - Объекты-значения (Value Objects): Это неизменяемые вспомогательные персонажи.
Адрес,Деньги,Цвет. У них нет ID. Два адреса с улицей "Ленина, 1" — это один и тот же адрес, пофиг, что объекты в памяти разные. Их создали, использовали и выкинули. Простые и надежные. - Агрегаты (Aggregates): Это такая семейная ячейка. Допустим,
Заказ(корень агрегата) и все егоПозицииЗаказа. Менять позиции можно ТОЛЬКО через методы самого заказа. Никто снаружи не может взять и просто так удалить позицию, минуя все проверки заказа. Это чтобы целостность не нарушить. Агрегат — это единица сохранности и согласованности. - Доменные сервисы (Domain Services): Иногда логика такая здоровая, что её некуда приткнуть. Не принадлежит она ни одному заказу или пользователю в отдельности. Например, сложный расчёт комиссии, который需要考虑 кучу факторов. Вот для этой логики, которая "висит в воздухе", и делают сервисы. Но злоупотреблять — не надо, а то получится анархия.
- Интерфейсы репозиториев (Repository Interfaces): Это обещания. Домен говорит: "Мне нужно уметь сохранять и загружать заказы, вот такой контракт". А как именно это будет делать PostgreSQL, MongoDB или твоя память — ему, блядь, похуй. Реализацию этих интерфейсов пишешь уже в другом месте, в слое инфраструктуры.
- События домена (Domain Events): Способ вежливо крикнуть на всю систему: "Эй, народ, у меня тут заказ завершился, может, кому надо?". Чтобы другие части приложения (например, сервис нотификаций) могли отреагировать, не лезя в кишки домену.
Вот смотри на пример, как это может выглядеть в коде. Сущность Заказ, которая сама за собой следит:
public class Order : Entity<int> // Сущность с целочисленным ID
{
// Состояние спрятано, трогать его снаружи — низзя.
private readonly List<OrderLine> _lines = new();
public CustomerId CustomerId { get; private set; } // Это Value Object для ID заказчика
public OrderStatus Status { get; private set; }
// Наружу отдаём только для чтения, чтобы никто не накосячил
public IReadOnlyCollection<OrderLine> Lines => _lines.AsReadOnly();
// Единственный способ изменить заказ — вызвать его метод.
// Здесь и живут все бизнес-правила, как в крепости.
public void AddItem(Product product, int quantity)
{
// Правило 1: В завершённый или отменённый заказ хуй что добавишь.
if (Status == OrderStatus.Completed || Status == OrderStatus.Cancelled)
throw new InvalidOrderOperationException("Нельзя изменять завершённый или отменённый заказ.");
// Правило 2: Количество должно быть нормальным, а не нулём или минусом.
if (quantity <= 0)
throw new ArgumentException("Количество должно быть больше нуля, гений.", nameof(quantity));
// Правило 3: Если товар уже есть — увеличиваем количество, а не создаём новую строку.
var existingLine = _lines.FirstOrDefault(l => l.ProductId == product.Id);
if (existingLine != null)
{
existingLine.IncreaseQuantity(quantity);
}
else
{
_lines.Add(new OrderLine(product.Id, product.Price, quantity));
}
}
public void MarkAsCompleted()
{
// Правило: Пустой заказ завершить нельзя, это же пиздец.
if (!_lines.Any())
throw new InvalidOrderOperationException("Нельзя завершить пустой заказ.");
Status = OrderStatus.Completed;
// Тут можно нагенерить Domain Event, типа OrderCompletedEvent(this.Id), чтобы все узнали.
}
}
И зачем весь этот цирк? А вот зачем:
- Тестируемость — овердохуищная. Ты можешь взять этот класс
Order, создать его в памяти, вызывать методы и проверять, как он себя ведёт. Никаких баз данных, HTTP-запросов, конфигов. Чистая логика. Это быстро и надёжно. - Сохраняемость. Бизнес-правила не потеряются где-то между контроллером и SQL-запросом. Они здесь, в одном месте, и с ними ничего не случится, даже если ты завтра переедешь с Entity Framework на Dapper.
- Гибкость. Захотел сменить базу данных? Пожалуйста! Реализуй интерфейсы репозиториев под новую базу в слое инфраструктуры, а доменный слой даже не чихнёт. Он про это ничего не знает и знать не хочет.
- Ясность. Открываешь код сущности — и сразу видишь, как работает бизнес. "А,
AddItemбросает исключение, если заказ завершён. Значит, так и задумано". Это упрощает жизнь и тебе, и новичкам в проекте, и даже, прости господи, общению с аналитиками.
Короче, если не хочешь через год поддерживать монстра, в котором всё перемешано и любое изменение вызывает волну непредсказуемых багов — учись выделять это ядро. Сначала кажется, что это овердизайн для маленького проекта, но поверь, привычка писать так — это как умение класть кирпичи ровно. Окупается всегда.