Что такое доменный слой (Domain Layer) в архитектуре приложения?

Ответ

Доменный слой — это ядро приложения, содержащее бизнес-логику, правила и сущности предметной области. Он полностью независим от инфраструктуры (БД, HTTP, фреймворка) и слоя представления. В моих проектах на PHP (часто с DDD-подходом) он включает:

  • Сущности (Entities): Объекты с идентификатором, жизненным циклом (например, User, Order).
  • Объекты-значения (Value Objects): Неизменяемые объекты без идентификатора (например, EmailAddress, Money).
  • Агрегаты (Aggregates): Кластер связанных сущностей и VO с корневым агрегатом (например, Order как корень с OrderItem внутри).
  • Доменные сервисы (Domain Services): Логика, не принадлежащая естественным образом какой-либо сущности.
  • События домена (Domain Events): Факты, произошедшие в домене (например, OrderWasPlaced).

Пример сущности Order с бизнес-правилами:

class Order
{
    private string $id;
    private OrderStatus $status;
    /** @var OrderLine[] */
    private array $lines = [];
    private ?DateTimeImmutable $paidAt = null;

    private function __construct(string $id) {
        $this->id = $id;
        $this->status = OrderStatus::NEW;
    }

    public static function create(string $id): self {
        return new self($id);
    }

    public function addLine(Product $product, int $quantity): void {
        // Доменное правило: нельзя изменить оплаченный заказ
        if ($this->status === OrderStatus::PAID) {
            throw new DomainException('Cannot modify a paid order.');
        }
        // Доменное правило: минимальное количество
        if ($quantity < 1) {
            throw new DomainException('Quantity must be at least 1.');
        }
        $this->lines[] = new OrderLine($product, $quantity);
    }

    public function markAsPaid(): void {
        // Доменное правило: оплатить можно только новый заказ
        if ($this->status !== OrderStatus::NEW) {
            throw new DomainException('Only new orders can be paid.');
        }
        $this->status = OrderStatus::PAID;
        $this->paidAt = new DateTimeImmutable();
        $this->recordEvent(new OrderPaid($this->id)); // Генерация события
    }

    public function totalAmount(): Money {
        // Вычисление на основе правил домена
        $total = Money::USD(0);
        foreach ($this->lines as $line) {
            $total = $total->add($line->subtotal());
        }
        return $total;
    }
}

Главный принцип: Доменный слой — это самая важная и стабильная часть. Его можно протестировать изолированно, без БД или веб-сервера, что я и делаю с помощью юнит-тестов.

Ответ 18+ 🔞

Давай разжую, что такое этот ваш доменный слой, а то некоторые думают, что это просто папка Domain/ в проекте, куда скидывают всё подряд, как в помойку.

Представь, что твоё приложение — это мужик. Так вот, доменный слой — это его мозг и характер. Всё, что он знает про мир (как работает заказ, что такое деньги, кто такой пользователь) и все его принципы («я не работаю по выходным», «не дам в долг больше тысячи»). А всё остальное — база данных, HTTP-запросы, красивые кнопки на фронте — это уже его руки, ноги и кошелёк. Мозг должен работать, даже если кошелёк украли, а руки отрубили. Понимаешь? Полная независимость, ёпта.

Вот из чего этот мозг состоит, если ковырять:

  • Сущности (Entities): Это главные герои, у которых есть паспорт (ID) и своя история. Пользователь, Заказ. Они живут, меняются, но остаются собой.
  • Объекты-значения (Value Objects): Это как купюры или почтовые адреса. EmailAddress, Money. Две сотни рублей — это те же две сотни рублей, неважно, какие у них серийные номера. Они неизменяемые, и это, блядь, очень важно.
  • Агрегаты (Aggregates): Это когда несколько сущностей сбиваются в кучку под предводительством одного главного — корня агрегата. Например, Заказ (корень) и внутри него ПозицииЗаказа. Трогать позиции можно только через заказ, иначе получится пиздец и несогласованность.
  • Доменные сервисы (Domain Services): Иногда появляется логика, которая не лезет ни в одну сущность. Ну типа «перевести деньги с одного счёта на другой». Это не поведение счёта и не поведение пользователя — это отдельная операция. Вот для этого они.
  • События домена (Domain Events): Это когда в домене что-то охуенное или не очень произошло, и надо об этом громко объявить. ЗаказБылОплачен. Чтобы другие части мозга (или даже другие системы) отреагировали: отправили смс, начислили бонусы.

А теперь смотри, как это выглядит в коде, без всей этой вашей шелухи инфраструктуры. Чистая бизнес-логика, ядрёна вошь!

class Order
{
    private string $id;
    private OrderStatus $status;
    /** @var OrderLine[] */
    private array $lines = [];
    private ?DateTimeImmutable $paidAt = null;

    private function __construct(string $id) {
        $this->id = $id;
        $this->status = OrderStatus::NEW;
    }

    public static function create(string $id): self {
        return new self($id);
    }

    public function addLine(Product $product, int $quantity): void {
        // Доменное правило номер раз: в оплаченный заказ нихуя нельзя пихать!
        if ($this->status === OrderStatus::PAID) {
            throw new DomainException('Cannot modify a paid order.');
        }
        // Правило два: ну ты хоть одну штуку-то закажи, а?
        if ($quantity < 1) {
            throw new DomainException('Quantity must be at least 1.');
        }
        $this->lines[] = new OrderLine($product, $quantity);
    }

    public function markAsPaid(): void {
        // А вот так: оплатить можно только свежий заказ, а не тот, который уже отменили.
        if ($this->status !== OrderStatus::NEW) {
            throw new DomainException('Only new orders can be paid.');
        }
        $this->status = OrderStatus::PAID;
        $this->paidAt = new DateTimeImmutable();
        $this->recordEvent(new OrderPaid($this->id)); // Бдыщ! И событие выстрелило
    }

    public function totalAmount(): Money {
        // И тут не просто сложение, а могут быть свои заморочки: скидки, налоги.
        $total = Money::USD(0);
        foreach ($this->lines as $line) {
            $total = $total->add($line->subtotal());
        }
        return $total;
    }
}

И главная мысль, которую ты должен вынести, чувак: этот слой — святая святых. Его можно взять и протестировать голым, без базы данных, без веб-сервера, на голом PHPUnit. Просто создал объекты и проверяешь, работают ли твои бизнес-правила. Если эта часть работает, значит, ядро твоего приложения живое. А всё остальное — просто обвязка, которую можно хоть завтра выкинуть и переписать. Доверия к инфраструктуре — ебать ноль, а к домену — сто пудово.