Моделирование домена
В сердце любого приложения, ориентированного на предметную область, находится модель, которая представляет собой основное знание и бизнес-правила системы. Хорошее моделирование предметной области означает преобразование концепций реального мира в выразительные, связные и последовательные структуры программного обеспечения.
Сущности
Сущность — это объект, который определяется главным образом своей идентичностью, а не только своими атрибутами. Даже если атрибуты меняются со временем, идентичность сущности остается неизменной.
Основные характеристики:
- Имеет уникальную идентичность (обычно
Id). - Важен именно кто сущность, а не только что она содержит.
- Её атрибуты могут изменяться со временем.
Создание сущности с помощью Lino
Для создания новой сущности с использованием Lino выполните:
lino entity new
CLI помощник запросит:
- Сервис – сервис, в котором будет создана сущность.
- Модуль – модуль, в котором будет создана сущность (только для модульных сервисов).
- Имя сущности – имя, используемое в домене и таблице базы данных.
Затем вы определите поля, которые составляют сущность, настроив каждое из них.
Доступные типы полей
| Тип | Описание | Диапазон / Примечания |
|---|---|---|
short | Целое число 16 бит | -32 768 → 32 767 |
int | Целое число 32 бита | -2 147 483 648 → 2 147 483 647 |
long | Целое число 64 бита | -9 223 372 036 854 775 808 → 9 223 372 036 854 775 807 |
string | Текст | До ~2 миллиардов символов |
bool | Булево значение | true или false |
Guid | Глобальный уникальный идентификатор | Распределённая уникальность |
decimal | Десятичное число с высокой точностью | Идеально для денежных значений |
float | Число с плавающей точкой (32 бита) | ≈ 6–9 знаков точности |
double | Число с плавающей точкой (64 бита) | ≈ 15–17 знаков точности |
DateTime | Дата и время | Включая часовой пояс |
DateOnly | Только дата (C# 10+) | – |
TimeOnly | Только время (C# 10+) | – |
Entity | Ссылка на другую сущность | 1 : 1 или 1 : N |
Value Object | Неизменяемый объект значения | Например: адрес, CPF |
Enum | Перечисление | Фиксированный набор значений |
List<Entity> | Список сущностей | 1 : N |
ManyToMany | Многие-ко-многим | Требуется таблица связей |
Пример
Создание сущности Person:
┌────┬────┬───────────────┬────────┬────────┬───────────┬────────────────┐ │ PK │ FK │ Property name │ Type │ Length │ Required │ Auto-increment │ ├────┼────┼───────────────┼────────┼────────┼───────────┼────────────────┤ │ x │ │ Id │ int │ │ x │ x │ ├────┼────┼───────────────┼────────┼────────┼───────────┼────────────────┤ │ │ │ Name │ string │ 100 │ x │ │ └────┴────┴───────────────┴────────┴────────┴───────────┴────────────────┘
Структура, сгенерированная Lino:
<ProjectName>/
└── src/
└── Services/
└── <ServiceName>/
└── Domain/
├── <ProjectName>.<ServiceName>.Domain.csproj
└── Aggregates/
└── People/
├── Person.cs
├── Errors/
│ └── PersonErrors.cs
├── Repositories/
│ └── IPersonRepository.cs
└── Resources/
└── Person/
├── PersonResources.resx
├── PersonResources.en.resx
└── PersonResources.pt-BR.resx
После определения сущностей используйте сам Lino для управления Migrations и синхронизации базы данных. Мы подробно рассмотрим этот процесс в разделе «Слой Персистенции».
Текущий workflow эволюции
Помимо интерактивного сценария lino entity new, CLI также позволяет явно выразить намерение в команде, когда сервис, модуль и имя уже известны:
lino entity new --name <EntityName> --service <ServiceName> --module <ModuleName> lino entity edit --service <ServiceName> --module <ModuleName> --entity <EntityName> lino entity list --service <ServiceName> --module <ModuleName>
Используйте entity list перед созданием новых концепций в больших модулях. Во время редактирования проверяйте идентификатор, свойства, обязательность, длину, связи, индексы, ownership, tenant и влияние на migrations.
Strongly Typed IDs, ownership и invariants
Когда включены Strongly Typed IDs, идентификатор становится выделенным типом, например ProductId, что снижает риск случайной подмены IDs разных сущностей. Связи должны отражать реальный ownership в домене: не каждая ссылка должна становиться прямой навигацией; часто идентификатор, shadow entity, integration event или явный запрос лучше защищает границы между модулями.
Размещайте invariants в домене, а не только в UI, базе данных или validators. После изменения сущностей выполните build, проверьте diff и сгенерируйте или обновите migrations.
Объекты значения
Объект значения представляет собой концепцию домена, определяемую только своими атрибутами – у него нет собственной идентичности. Два объекта значения считаются равными, если все их значения совпадают.
Основные характеристики:
- Неизменяемы после создания.
- Не имеют
Id.
Создание объекта значения с помощью Lino
Выполните:
lino value-object new
CLI запросит:
- Сервис – Сервис, в котором будет создан объект.
- Модуль – Модуль, в котором будет создан объект (только для модульных сервисов).
- Расположение – Корень домена или конкретный агрегат.
- Имя объекта значения.
Затем определите поля, которые составляют объект.
Доступные типы полей
| Тип | Описание | Примечания |
|---|---|---|
short | 16-битное целое | -32 768 → 32 767 |
int | 32-битное целое | -2 147 483 648 → 2 147 483 647 |
long | 64-битное целое | -9 223 372 036 854 775 808 → 9 223 372 036 854 775 807 |
string | Текст | До ~2 миллиардов символов |
bool | Булево | true/false |
decimal | Точный десятичный | Денежные значения |
float | Число с плавающей точкой (32 бита) | ≈ 6–9 цифр |
double | Число с плавающей точкой (64 бита) | ≈ 15–17 цифр |
DateTime | Дата/время | Включая часовой пояс |
DateOnly | Только дата | C# 10+ |
TimeOnly | Только время | C# 10+ |
Пример
Объект значения Address:
┌───────────────┬────────┬────────┬───────────┐ │ Property name │ Type │ Length │ Required │ ├───────────────┼────────┼────────┼───────────┤ │ Street │ string │ 100 │ x │ ├───────────────┼────────┼────────┼───────────┤ │ Number │ string │ 10 │ x │ ├───────────────┼────────┼────────┼───────────┤ │ Neighborhood │ string │ 50 │ │ ├───────────────┼────────┼────────┼───────────┤ │ City │ string │ 100 │ x │ ├───────────────┼────────┼────────┼───────────┤ │ State │ string │ 2 │ x │ ├───────────────┼────────┼────────┼───────────┤ │ PostalCode │ string │ 20 │ x │ ├───────────────┼────────┼────────┼───────────┤ │ Country │ string │ 100 │ x │ └───────────────┴────────┴────────┴───────────┘
Структура сгенерированных файлов (агрегат Person):
<ProjectName>/
└── src/
└── Services/
└── <ServiceName>/
└── Domain/
├── <ProjectName>.<ServiceName>.Domain.csproj
└── Aggregates/
└── People/
├── Person.cs
├── ValueObjects/
│ └── Address.cs
├── Errors/
│ ├── AddressErrors.cs
│ └── PersonErrors.cs
├── Repositories/
│ └── IPersonRepository.cs
└── Resources/
├── Address/
│ ├── AddressResources.resx
│ ├── AddressResources.en.resx
│ └── AddressResources.pt-BR.resx
└── Person/
├── PersonResources.resx
├── PersonResources.en.resx
└── PersonResources.pt-BR.resx
Как и в сущностях, Migrations могут управляться Lino для поддержания синхронизации модели данных.
Текущий workflow эволюции
Помимо интерактивной команды, используйте параметры, когда уже известно, где должна быть создана концепция:
lino value-object new --name <ValueObjectName> --service <ServiceName> --module <ModuleName> lino value-object edit --service <ServiceName> --module <ModuleName> --value-object <ValueObjectName> lino value-object list --service <ServiceName> --module <ModuleName>
Когда свойство сущности объявлено как ValueObject, Lino может переиспользовать существующий Value Object или создать новый во время моделирования сущности. Предпочитайте этот вариант, когда набор полей представляет одну концепцию, например деньги, адрес, размеры, период или документ.
Персистентность, UI и локализация
Lino отображает свойства Value Objects через слой персистентности модуля и переносит метаданные отображения в resources. Это позволяет создавать локализованные labels и сообщения для вложенных значений. Value Object принадлежит модулю, в котором он был сгенерирован; он не становится общим контрактом между модулями без явной integration.
Защищайте invariants в самом типе: отрицательные значения, некорректная валюта, перевернутые периоды или неправильно сформированные документы не должны создаваться валидными.
Перечисления
Перечисления в DDD могут выходить за рамки традиционных enum в C#. Они могут быть богатыми объектами,
представляющими фиксированные состояния, содержащими валидации, вспомогательные методы и даже поведение.
Мотивация:
- C#
enumограничены целочисленным или строковым значением. - Моделирование перечисления как класса предоставляет большую гибкость и выразительность.
Основные характеристики:
- Это классы, наследующиеся от общего базового класса и инкапсулирующие
IdиИмя. - Позволяют добавлять валидации, вспомогательные методы и поведение.
Создание перечисления с помощью Lino
Выполните:
lino enumeration new
Мастер запросит:
- Сервис.
- Модуль (если применимо).
- Расположение – корень домена или агрегат.
- Имя перечисления.
- Тип – традиционный
enumили Smart Enum (class). - Хранение –
intилиstringв базе данных.
Пример
Перечисление PersonStatus:
┌───────┬───────────┬──────────────┐ │ Value │ Name │ Display Name │ ├───────┼───────────┼──────────────┤ │ 1 │ Active │ Active │ ├───────┼───────────┼──────────────┤ │ 2 │ Inactive │ Inactive │ ├───────┼───────────┼──────────────┤ │ 3 │ Suspended │ Suspended │ ├───────┼───────────┼──────────────┤ │ 4 │ Deleted │ Deleted │ └───────┴───────────┴──────────────┘
Сгенерированная структура:
<ProjectName>/
└── src/
└── Services/
└── <ServiceName>/
└── Domain/
├── <ProjectName>.<ServiceName>.Domain.csproj
└── Aggregates/
└── People/
├── Person.cs
├── Enums/
│ └── PersonStatus.cs
├── ValueObjects/
│ └── Address.cs
├── Errors/
│ ├── AddressErrors.cs
│ └── PersonErrors.cs
├── Repositories/
│ └── IPersonRepository.cs
└── Resources/
├── Address/
│ ├── AddressResources.resx
│ ├── AddressResources.en.resx
│ └── AddressResources.pt-BR.resx
├── Person/
│ ├── PersonResources.resx
│ ├── PersonResources.en.resx
│ └── PersonResources.pt-BR.resx
└── PersonStatus/
├── PersonStatusResources.resx
├── PersonStatusResources.en.resx
└── PersonStatusResources.pt-BR.resx
Хранить значение перечисления как string допустимо и может улучшить читаемость, но обычно менее эффективно по производительности и хранению.
Поэтому рекомендуется хранить значение как int, а для сохранения ссылочной целостности и облегчения сопровождения создать вспомогательную сущность (таблицу),
где первичный ключ соответствует значению перечисления.
После определения перечислений используйте Lino для генерации и применения Migrations, гарантируя, что база данных отражает модель домена. Подробнее см. в разделе Слой Персистентности.
Текущий workflow эволюции
Помимо интерактивной команды, используйте параметры, когда расположение и имя уже определены:
lino enumeration new --name <EnumerationName> --service <ServiceName> --module <ModuleName> lino enumeration edit --service <ServiceName> --module <ModuleName> --enumeration <EnumerationName> lino enumeration list --service <ServiceName> --module <ModuleName>
Enumeration, сущность или конфигурация
| Использовать enumeration | Использовать сущность или конфигурацию |
|---|---|
| Статус заказа, состояние публикации, технический тип интеграции | Категория, управляемая пользователем, причина, настраиваемая для tenant, поставщик, управляемый в backoffice |
| Значения, версионируемые вместе с кодом | Значения, изменяемые в runtime или контролируемые правами доступа |
| Простое правило или небольшое поведение для каждого значения | Жизненный цикл, audit, динамический перевод или собственная связь |
Если enumeration предоставляет отображаемое имя, Lino может сгенерировать resources для локализованных labels. Если она сохраняется или используется как seed data, проверьте migrations после добавления, удаления или переименования значений. Согласованность кода, базы данных и UI является частью изменения домена.
