Моделирование домена

В сердце любого приложения, ориентированного на предметную область, находится модель, которая представляет собой основное знание и бизнес-правила системы. Хорошее моделирование предметной области означает преобразование концепций реального мира в выразительные, связные и последовательные структуры программного обеспечения.

Сущности

Сущность — это объект, который определяется главным образом своей идентичностью, а не только своими атрибутами. Даже если атрибуты меняются со временем, идентичность сущности остается неизменной.

Основные характеристики:

  • Имеет уникальную идентичность (обычно 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 запросит:

  • Сервис – Сервис, в котором будет создан объект.
  • Модуль – Модуль, в котором будет создан объект (только для модульных сервисов).
  • Расположение – Корень домена или конкретный агрегат.
  • Имя объекта значения.

Затем определите поля, которые составляют объект.

Доступные типы полей

ТипОписаниеПримечания
short16-битное целое-32 768 → 32 767
int32-битное целое-2 147 483 648 → 2 147 483 647
long64-битное целое-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 является частью изменения домена.

Произошла необработанная ошибка. Обновить 🗙