Опасность... Опасность Д-р Смит... Философский пост впереди
Цель этого сообщения заключается в том, чтобы определить, действительно ли размещение логики проверки вне моих доменных сущностей (фактический совокупный корень) предоставляет мне большую гибкость или код kamikaze
В принципе, я хочу знать, есть ли лучший способ проверить мои объекты домена. Вот как я планирую это сделать, но я бы хотел, чтобы ваше мнение
Первый подход, который я рассматривал, заключался в следующем:
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 (Open for extension, но Close для модификации), и я обнаружил, что нарушая этот принцип, обслуживание кода становится реальной болью, когда приложение растет по сложности. Зачем? Поскольку правила домена меняются чаще, чем мы хотели бы признать, и если правила скрыты и внедрены в сущности, подобной этой, их трудно проверить, трудно читать, трудно поддерживать, но реальные причина, почему мне не нравится этот подход: если правила проверки меняются, я должен прийти и отредактировать объект домена. Это был очень простой пример, но в RL проверка может быть более сложной
Итак, следуя философии Udi Dahan, , делая роли явным, и рекомендацию Эрика Эванса в синей книге, следующей попыткой было реализовать шаблон спецификации, что-то вроде этого
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("...");
}
}
Ну, это основная идея, сущность передается в валидатор, если нам нужно какое-то значение из сущности для выполнения проверки, команда содержит данные, поступающие от пользователя, и поскольку валидаторы считаются инъекционными, в которые они могли бы вставлять внешние зависимости, если это требует проверка.
Теперь дилемма, я доволен дизайном, потому что моя проверка инкапсулирована в отдельные объекты, что приносит много преимуществ: easy unit test, легко поддерживается, инварианты домена явно выражаются с помощью Ubiquitous Language, легко расширяемый, логика проверки является централизованной, и валидаторы могут использоваться вместе для обеспечения соблюдения сложных правил домена. И даже когда я знаю, что я размещаю проверку своих сущностей вне их (вы можете утверждать, что запах кода - Anemic Domain), но я думаю, что компромисс допустим
Но есть одна вещь, которую я не понял, как ее реализовать чистым способом. Как использовать эти компоненты...
Так как они будут введены, они не будут естественно входить в мои сущности домена, поэтому в основном я вижу два варианта:
-
Передайте валидаторы для каждого метода моего объекта
-
Проверять мои объекты извне (из обработчика команд)
Я не доволен вариантом 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, когда придет команда, будет обработчик команд, получающий доступ к совокупному корню и выполняя операции против совокупного корня, создавая только идеальное окно для размещения проверки.
Я хотел бы сделать небольшое сравнение преимуществ для размещения проверки внутри объекта и размещения его в отдельных объектах
-
Проверка в отдельных объектах
- Pro. Легко писать
- Pro. Легко проверить
- Pro. Он явно выражал
- Pro. Он становится частью дизайна домена, выраженным текущим вездесущим языком.
- Pro. Поскольку он теперь является частью дизайна, он может быть смоделирован с использованием диаграмм UML.
- Pro. Чрезвычайно прост в обслуживании.
- Pro. Делает мои сущности и логику проверки слабо связанными
- Pro. Простота расширения
- Pro. После SRP
- Pro. Следуя принципу Open/Close
- Pro. Не нарушать закон Деметры (ммм)?
- Pro. Я централизованно
- Pro. Он может быть повторно использован
- Pro. При необходимости внешние зависимости можно легко вводить
- Pro. Если использовать подключаемую модель, новые валидаторы могут быть добавлены просто путем удаления новых сборок без необходимости повторной компиляции всего приложения.
- Pro. Реализация механизма правил будет проще
- Con. Нарушение инкапсуляции
- Con. Если инкапсуляция является обязательной, нам нужно будет передать отдельные валидаторы методу сущности (агрегата)
-
Проверка, заключенная внутри объекта
- Pro. Герметичная?
- Pro. Многоразовый?
Я хотел бы прочитать ваши мысли об этом