TGViewer
.NET Разработчик .NET Разработчик @netdeveloperdiary · 6.75K subscribers
Post #3191 1.83K
День 2664. #ЗаметкиНаПолях #DDD
Что Такое Инварианты? Окончание

Начало

Модель как источник истины
Три шага к цели
1. Запрет создания недействительных объектов
Курс без названия не должен существовать — делаем это невозможным:
public class Course
{
private Course(CourseId id, string title, Money price)
{
Id = id;
Title = title;
Price = price;
Status = CourseStatus.Draft;
}

public static Result<Course>
Create(string title, Money price)
{
if (string.IsNullOrWhiteSpace(title))
return CourseErrors.TitleRequired;

return new Course(CourseId.New(), title, price);
}

Приватный конструктор со статической фабрикой предоставляет единственный вариант создания объекта, и именно там выполняется валидация. Теперь любой код, работающий с объектом Course, может предполагать, что у него есть допустимое название.
Объекты-значения, такие как Money, применяют ту же идею: Money не может быть отрицательным или обязан иметь тип валюты.

2. Инкапсуляция переходов состояния
Класс должен контролировать, как он изменяется, вместо того чтобы оставлять это на усмотрение его обработчиков. Никаких сеттеров, и каждое изменение проходит через метод, который знает правила:
public Result Publish(IDateTimeProvider clock)
{
if (Status != CourseStatus.Draft)
return CourseErrors.AlreadyPublished;

if (_lessons.Count == 0)
return CourseErrors.CannotPublishWithoutLessons;

Status = CourseStatus.Published;
PublishedOn = clock.UtcNow;
return Result.Success();
}

Обработчику не нужно знать, был ли курс уже опубликован, или помнить о проверке на наличие уроков. Он вызывает Publish и использует полученный результат. Правило находится рядом с состоянием, которое оно защищает.

3. Инкапсуляция агрегата
Некоторые правила охватывают несколько сущностей в пределах одного агрегата. Корень агрегата — правильное место для обеспечения их соблюдения.

Например, правило: опубликованный курс должен содержать как минимум один урок, и уроки нельзя удалять после публикации.

Неправильный подход — предоставлять Lessons как изменяемую коллекцию и полагаться на то, что обработчики будут соблюдать правило везде. Правильный подход — держать коллекцию приватной и принудительно обрабатывать каждое изменение через корень агрегата:
public class Course
{
private readonly List<Lesson> _lessons = [];
public IReadOnlyCollection<Lesson>
Lessons => _lessons.AsReadOnly();

public Result RemoveLesson(LessonId id)
{
if (Status == CourseStatus.Published)
return CourseErrors.CannotModifyPublishedLessons;

var lesson = _lessons.FirstOrDefault(l => l.Id == id);
if (lesson is null)
return CourseErrors.LessonNotFound;

_lessons.Remove(lesson);
return Result.Success();
}
}

Когда правило должно охватывать два агрегата вместо одного, это уже другая проблема, тут лучше использовать событие предметной области, а не позволять одному агрегату влиять на другой.

Итого
Можно написать ту же систему процедурно, и она может хорошо работать, но вы теряете доверие. В процедурной системе каждый вызывающий объект несёт ответственность за то, чтобы не нарушать правила. В постоянно действительной модели эта ответственность лежит на самой модели. Разница накапливается со временем:
- Валидаторы не меняются, потому что нечего дублировать.
- Проверки кода фокусируются на поведении, а не на «не забыли ли мы проверить Х?».
- Новые конечные точки не могут случайно обойти правило, находящееся в сущности.
- В тестах не нужно проверять недействительные сценарии.
- Модель из пассивного носителя данных превращается в минимальное и чёткое место хранения бизнес-правил.

В основе этого лежит инкапсуляция. Модель инкапсулирует правила, управляющие её состоянием, а остальная часть системы взаимодействует с ней через чётко определённый интерфейс. Это приводит к более чистому коду, меньшему количеству ошибок и в целом к более удобной в сопровождении системе.

Источник:
https://www.milanjovanovic.tech/blog/what-invariants-are-and-why-a-domain-model-is-the-best-place-to-enforce-them
  • 👍 14
  • 👎 1
More from @netdeveloperdiary
  1. Sep 27, 2026День 2797. #ЗаметкиНаПолях #AI Рабочий процесс с Copilot для .NET. Окончание Начало Продол…
  2. Sep 26, 2026День 2796. #ЗаметкиНаПолях #AI Рабочий процесс с Copilot для .NET. Продолжение Начало Три…
  3. Sep 25, 2026День 2795. #ЗаметкиНаПолях #AI Рабочий процесс с Copilot для .NET. Начало Проблема с позиц…
  4. Sep 24, 2026День 2794. #Оффтоп #Здоровье Сегодня будет необычный пост. Завтра в Москве стартует конфер…
  5. Sep 23, 2026День 2793. #ЗаметкиНаПолях #SQL 10 Редких Возможностей SQL, Которые Стоит Знать Каждому. Ч…
  6. Sep 22, 2026День 2792. #ЗаметкиНаПолях #SQL 10 Редких Возможностей SQL, Которые Стоит Знать Каждому. Ч…
Threads Profile ViewerView any public Threads profile without an account.Open ThreadLook →Writing with AI? Make it sound human.Metric37 rewrites AI drafts so they read naturally. Free AI detector, 1,500 words free.Try Metric37 →