Теперь разберёмся, а что мы размещаем в домене? Как правило это:
- сущности (Entities)
- интерфейсы (Interfaces)
- бизнес-правила (Business Rules)
- доменные ошибки (Domain Errors)
Как видите, здесь нет ничего из «внешнего мира». В том числе и декораторов от ORM. Взглянем на пример сущности из демонстрационного репозитория:
export class User {
constructor(
public readonly id: string,
public readonly email: string,
public readonly passwordHash: string,
) {}
}
Это самый обычный класс, без каких-либо декораторов от ORM или других примочек для сериализации. Нет здесь и импорта каких-либо библиотек. В более реалистичном варианте сюда могут быть добавлены инварианты. Например:
if (!email.includes('@')) {
throw new InvalidEmailError()
}
Именно здесь следует проверить корректность состояния сущности, а не в контроллере или, скажем, в репозитории.
Теперь посмотрим на контракт для репозитория. Он выглядит так:
export interface UserRepository {
save(user: User): Promise<void>
findByEmail(email: string): Promise<User | null>
}
Домен определяет, ЧТО ему необходимо, но ничего не говорит о способе реализации. Она может быть какой угодной. Домен говорит, что ему нужен способ сохранить и найти пользователя. Как это будет реализовано — его абсолютно не волнует. По факту это и есть Dependency Inversion на практике.
🎯 Простой чек-лист
Выполнить проверку и найти нарушения не сложно. Само собой, в реальном проекте для этого настраиваются дополнительные правила линтера. Но есть ещё один способ. Он отлично подходит, когда вы только начинается вносить изменения в доменный слой. Просто задайте себе вопрос: сможет ли работать ваша бизнес-логика если вы удалите, например, SQLite / Express / Prisma / Axios или любую-другую библиотеку. Если ответ — «Да», ваш домен действительно изолирован. Если «нет», значит нарушены правила и зависимости уже торчат наружу.
Итак, домен — это место, где формируются правила игры. Всё остальное помогает доставить данные внутрь и обратно.