Опасно... Опасно, доктор Смит... Впереди философский пост
Цель этого поста — определить, действительно ли размещение логики проверки за пределами объектов домена (совокупный корень) предоставляет мне больше гибкости или это код камикадзе
По сути, я хочу знать, есть ли лучший способ проверки моих доменных объектов. Вот как я планирую это сделать, но я хотел бы узнать ваше мнение.
Первый подход, который я рассматривал, был следующим:
class Customer : EntityBase<Customer>
{
public void ChangeEmail(string email)
{
if(string.IsNullOrWhitespace(email)) throw new DomainException(“...”);
if(!email.IsEmail()) throw new DomainException();
if(email.Contains(“@mailinator.com”)) throw new DomainException();
}
}
На самом деле мне не нравится эта проверка, потому что даже когда я инкапсулирую логику проверки в правильную сущность, это нарушает принцип Open/Close (открыть для расширения, но закрыть для модификации), и я обнаружил, что нарушение этого принципа, обслуживание кода становится настоящей проблемой, когда приложение становится сложнее. Почему? Поскольку правила домена меняются чаще, чем нам хотелось бы признать, и если правила скрыты и встроеныв сущность, подобную этой, их трудно тестировать, трудно читать, трудно поддерживать, но настоящая причина почему мне не нравится такой подход: если правила проверки меняются, я должен прийти и отредактировать свою доменную сущность.Это был действительно простой пример, но в RL проверка может быть более сложной
. Итак, следуя философии Уди Дахана, явному определению ролейи рекомендации Эрика Эванса в синей книге, следующий попытаться было реализовать шаблон спецификации, что-то вроде этого
class EmailDomainIsAllowedSpecification : IDomainSpecification<Customer>
{
private INotAllowedEmailDomainsResolver invalidEmailDomainsResolver;
public bool IsSatisfiedBy(Customer customer)
{
return !this.invalidEmailDomainsResolver.GetInvalidEmailDomains().Contains(customer.Email);
}
}
. Но затем я понял, что для того, чтобы следовать этому подходу, мне пришлось сначала изменить свои объекты, чтобы передать значение, которое проверяется , в этом случае электронной почты, но их изменение приведет к срабатыванию событий моего домена, чего я не хотел бы, пока новое электронное письмо не станет действительным
. Архитектура CQRS:
class EmailDomainIsAllowedValidator : IDomainInvariantValidator<Customer, ChangeEmailCommand>
{
public void IsValid(Customer entity, ChangeEmailCommand command)
{
if(!command.Email.HasValidDomain()) throw new DomainException(“...”);
}
}
Ну, это основная идея, сущность передается валидатору на тот случай, если нам потребуется какое-то значение от сущности для выполнения проверки, команда содержит данные, поступающие от пользователя, и поскольку валидаторы учитываются инъекционныепредметы, которые они могли внедрить внешние зависимости, если этого требует проверка.
Теперь дилемма, я доволен таким дизайном, потому что моя проверка инкапсулирована в отдельные объекты, что дает много преимуществ: простота модульного тестирования, простота обслуживания, доменные инварианты явно выражены с использованием Ubiquitous Language, простота для расширения логика проверки централизована, и валидаторы могут использоваться вместе для обеспечения соблюдения сложных правил предметной области.И даже когда я знаю, что размещаю проверку своих сущностей за их пределами (Можно утверждать, что это запах кода — Анемический Домен), но я думаю, что компромисс приемлем
Но есть одна вещь, которую я не понял. как реализовать это в чистом виде. Как мне использовать эти компоненты...
Поскольку они будут внедрены, они не будут естественным образом вписываться в мои доменные объекты, поэтому в основном я вижу два варианта:
Передать валидаторы каждому методу моего сущность
Проверить мои объекты извне (из обработчика команд)
Меня не устраивает вариант 1, поэтому я объясню, как бы я сделал это с вариантом 2
class ChangeEmailCommandHandler : ICommandHandler<ChangeEmailCommand>
{
// here I would get the validators required for this command injected
private IEnumerable<IDomainInvariantValidator> validators;
public void Execute(ChangeEmailCommand command)
{
using (var t = this.unitOfWork.BeginTransaction())
{
var customer = this.unitOfWork.Get<Customer>(command.CustomerId);
// here I would validate them, something like this
this.validators.ForEach(x =. x.IsValid(customer, command));
// here I know the command is valid
// the call to ChangeEmail will fire domain events as needed
customer.ChangeEmail(command.Email);
t.Commit();
}
}
}
Ну вот и все. Можете ли вы поделиться своими мыслями по этому поводу или поделиться своим опытом проверки объектов домена
РЕДАКТИРОВАТЬ
Я думаю, что это не ясно из моего вопроса, но реальная проблема заключается в следующем: сокрытие правил домена имеет серьезные последствия для будущего обслуживания. приложения, а также правила домена часто меняются в течение жизненного цикла приложения. Следовательно, реализация их с учетом этого позволит нам легко расширить их. Теперь представьте, что в будущем будет реализован механизм правил, если правила будут инкапсулированы вне объектов домена, это изменение будет легче реализовать.
Я знаю, что размещение проверки вне моих объектов нарушает инкапсуляцию, как упоминал @jgauffin в своем ответе, но я думаю, что преимущества размещения проверки в отдельных объектах гораздо более существенны, чем просто сохранение инкапсуляции объекта.Теперь я думаю, что инкапсуляция имеет больше смысла в традиционной n-уровневой архитектуре, потому что сущности использовались в нескольких местах доменного уровня, но в архитектуре CQRS, когда поступает команда, будет обработчик команды, обращающийся к совокупному корню и выполнение операций с совокупным корнем только создает идеальное окно для размещения проверки.
Я хотел бы провести небольшое сравнение между преимуществами размещения проверки внутри объекта по сравнению с ее размещением в отдельных объектах
Проверка в отдельных объектах
Validation, инкапсулированному внутри объекта
Хотелось бы прочитать ваши мысли по этому поводу