Проектирование проверок на уровне модели предметной области
Совет
Это содержимое является фрагментом из электронной книги, архитектуры микрослужб .NET для контейнерных приложений .NET, доступных в документации .NET или в виде бесплатного скачиваемого PDF-файла, который можно читать в автономном режиме.
В DDD правила проверки можно рассматривать как инварианты. Главной задачей агрегата является применение инвариантов в изменениях состояния для всех сущностей внутри него.
Сущности предметной области всегда должны быть допустимыми. Существует определенное количество инвариантов для объекта, которые всегда должны быть истинными. Например, для объекта позиции заказа всегда должно быть указано количество в виде положительного целого числа, а также название и цена изделия. Таким образом, за применение инвариантов отвечают сущности предметной области (особенно для корня агрегата), а право на существование имеет только допустимый объект сущности. Инвариантные правила выражаются в виде контрактов. При их нарушении вызываются исключения или выводятся уведомления.
Это обуславливается тем, что возникает множество ошибок, поскольку объекты находятся в состоянии, в котором они никогда не должны быть.
Предположим, что у нас есть служба SendUserCreationEmailService, принимающая UserProfile... Как в этой службе можно рационализировать тот момент, что имя имеет ненулевое значение? Нужно выполнять еще одну проверку? Или, что более вероятно, вы просто игнорируете проверку и надеетесь на лучшее — вы надеетесь, что до отправки вам проверку выполнил кто-то другой. Конечно, в рамках разработки на уровне тестирования один из первых создаваемых тестов должен проверять следующее условие: если сообщение отправляется клиенту с пустым именем, должна возникнуть ошибка. Но как только мы начинаем писать эти виды тестов снова и снова, мы понимаем, что если бы мы не разрешили использовать имя со значением NULL, то всех этих тестов не было бы.
Реализация проверок на уровне модели предметной области
Как правило, проверки обычно реализуются в конструкторах сущностей предметной области или в методах, которые могут обновлять сущность. Существует несколько способов реализации проверки, например проверка данных и вызов исключений, если проверка завершается ошибкой. Есть более сложные схемы, например использование шаблона спецификации для проверок и шаблона уведомления для возвращения коллекции ошибок вместо возвращения исключений, возникающих при каждой проверке.
Проверка условий и создание исключений
В следующем примере кода показан простейший способ проверки сущности предметной области путем создания исключения. В конце этого раздела приводятся ссылки на более сложные реализации на основе шаблонов, которые обсуждались ранее.
public void SetAddress(Address address)
{
_shippingAddress = address?? throw new ArgumentNullException(nameof(address));
}
В более наглядном примере демонстрируется необходимость обеспечить либо неизменность внутреннего состояния, либо возникновение всех изменений для метода. Например, в следующей реализации объект останется в недопустимом состоянии:
public void SetAddress(string line1, string line2,
string city, string state, int zip)
{
_shippingAddress.line1 = line1 ?? throw new ...
_shippingAddress.line2 = line2;
_shippingAddress.city = city ?? throw new ...
_shippingAddress.state = (IsValid(state) ? state : throw new …);
}
Если состояние имеет недопустимое значение, значит, первая строка адреса и город уже были изменены. В этом случае адрес может стать недействительным.
Аналогичный подход можно использовать в конструкторе сущности, когда вызывается исключение для проверки допустимости сущности после ее создания.
Использование атрибутов проверки в модели на основе заметок к данным
Заметки к данным, например атрибуты Required или MaxLength, можно использовать для настройки свойств полей базы данных EF Core, как подробно описывается в разделе Сопоставление таблиц, но они больше не работают для проверки сущностей в EF Core (как и метод IValidatableObject.Validate), как это было с версии EF 4.x в .NET Framework.
Заметки к данным и интерфейс IValidatableObject по-прежнему можно использовать для проверки модели во время привязки модели, до вызова действий контроллера, как обычно, но эта модель должна быть моделью представления или DTO, а это задача MVC или API, а не модели предметной области.
Учитывая это принципиальное различие, вы по-прежнему можете использовать заметки к данным и IValidatableObject
в классе сущности для проверки, если ваши действия получают параметр объекта класса сущности, что не рекомендуется. В этом случае проверка будет проведена после привязки модели, непосредственно перед вызовом действия, и вы можете проверить свойство ModelState.IsValid контроллера для проверки результата, но повторим: это происходит в контроллере, не до сохранения объекта сущности в DbContext, как это было с версией EF 4.x.
Можно реализовать пользовательскую проверку в классе сущности с помощью заметок к данным и метода IValidatableObject.Validate
путем переопределения метода SaveChanges для DbContext.
Вы видите пример реализации для проверки сущностей IValidatableObject
в этом комментарии на GitHub. В этом примере не выполняются проверки на основе атрибутов, но их легко реализовать с помощью отражения в том же переопределении.
Однако с точки зрения DDD наилучшая оптимизация модели предметной области достигается за счет использования исключений в методах поведения сущности или при реализации шаблонов спецификации и уведомления для применения правил проверки.
Заметки к данным целесообразно использовать на уровне приложения в классах ViewModel (а не в сущностях предметной области), которые будут принимать входные данные, чтобы осуществлять проверку модели на уровне пользовательского интерфейса. Однако это не должно происходить при исключении проверки в модели предметной области.
Проверка сущностей путем реализации шаблона спецификации и шаблона уведомлений
Наконец, более сложный подход к реализации проверок в модели предметной области заключается в реализации шаблона спецификации вместе с шаблоном уведомления, как описано в некоторых приведенных далее дополнительных ресурсах.
Следует отметить, что можно использовать только один из этих шаблонов. Например, можно выполнять проверки вручную с помощью операторов управления и применять шаблон уведомления для сбора и возвращения списка ошибок, обнаруженных при проверке.
Использование отложенной проверки в предметной области
Существуют различные методы обработки отложенных проверок в предметной области. Вон Вернон (Vaughn Vernon) рассматривает их в своей книге Implementing Domain-Driven Design (Реализация разработки на основе предметной области) в разделе, посвященном проверке.
Двухэтапная проверка
Рассмотрим двухэтапную проверку. Используйте проверку на уровне поля для объектов передачи данных (DTO) команд и проверку на уровне предметной области внутри сущностей. Это можно сделать путем возвращения результирующего объекта вместо исключений для упрощения работы с ошибками проверки.
Например, с помощью проверки полей с заметками к данным вы не дублируете определение проверки. Однако при использовании DTO (к примеру, команд и ViewModel) выполнение может осуществляться как на стороне сервера, так и на стороне клиента.
Дополнительные ресурсы
Рэйчел Аппель (Rachel Appel). Общие сведения о проверке модели в ASP.NET Core MVC
https://learn.microsoft.com/aspnet/core/mvc/models/validationРик Андерсон (Rick Anderson). Добавление проверки
https://learn.microsoft.com/aspnet/core/tutorials/first-mvc-app/validationМартин Фоулер (Martin Fowler). Замена исключений уведомлениями в проверках
https://martinfowler.com/articles/replaceThrowWithNotification.htmlСпецификация и шаблоны уведомлений
https://www.codeproject.com/Tips/790758/Specification-and-Notification-PatternsЛев Городинский (Lev Gorodinski). Проверка в проблемно-ориентированном программировании (DDD)
http://gorodinski.com/blog/2012/05/19/validation-in-domain-driven-design-ddd/Колин Джек (Colin Jack) Проверка модели предметной области
https://colinjack.blogspot.com/2008/03/domain-model-validation.htmlДжимми Богард (Jimmy Bogard). Проверка в мире DDD
https://lostechies.com/jimmybogard/2009/02/15/validation-in-a-ddd-world/