Вопрос по architecture, c#, validation – Проверка домена в архитектуре CQRS

43

Danger ... Danger Dr. Smith... Philosophical post ahead

Цель этого поста - определить, действительно ли размещение логики проверки вне сущностей моего домена (на самом деле, агрегатного корня) дает мне большую гибкость или этоkamikaze code

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

Первый подход, который я рассмотрел, был:

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();
   }
}

Мне действительно не нравится эта проверка, потому что даже когда я инкапсулирую логику проверки в правильную сущность, это нарушает принцип открытия / закрытия (открыт для расширения, но закрыт для модификации), и я обнаружил, что нарушение этого принципа приводит к поддержанию кода настоящая боль, когда приложение становится все сложнее. Зачем? Потому что правила домена меняются чаще, чем хотелось бы признать, и если правилаhidden and embedded в такой сущности их сложно протестировать, трудно прочитать, сложно поддерживать, но настоящая причина, по которой мне не нравится этот подход, заключается в следующем: если меняются правила проверки, я должен прийти и отредактировать свою сущность домена. Это был действительно простой пример, но в RL проверка может быть более сложной

Итак, следуя философии Уди Дахана,making roles explicitи рекомендация Эрика Эванса из синей книги, следующей попыткой было реализовать шаблон спецификации, что-то вроде этого

class EmailDomainIsAllowedSpecification : IDomainSpecification<Customer>
{
   private INotAllowedEmailDomainsResolver invalidEmailDomainsResolver;
   public bool IsSatisfiedBy(Customer customer)
   {
      return !this.invalidEmailDomainsResolver.GetInvalidEmailDomains().Contains(customer.Email);
   }
}

Но потом я понимаю, что для того, чтобы следовать этому подходу, мне сначала пришлось мутировать свои сущности, чтобы пройтиvalue being valdiatedв этом случае сообщение электронной почты, но с его изменением может привести к запуску событий моего домена, чего я бы не хотел, пока новое сообщение не станет действительным

So after considering these approaches, I came out with this one, since I am going to implement a CQRS architecture:

class EmailDomainIsAllowedValidator : IDomainInvariantValidator<Customer, ChangeEmailCommand>
{
   public void IsValid(Customer entity, ChangeEmailCommand command)
   {
      if(!command.Email.HasValidDomain())  throw new DomainException(“...”);
   }
}

Хорошо, что это основная идея, сущность передается в валидатор, если нам требуется какое-то значение от сущности для выполнения валидации, команда содержит данные, поступающие от пользователя, и поскольку валидаторы рассматриваютсяinjectable Объекты, в которые они могут вставлять внешние зависимости, если этого требует проверка.

Now the dilemmaЯ доволен таким дизайном, потому что моя валидация заключена в отдельные объекты, что дает много преимуществ: простота модульного тестирования, простота обслуживания, инварианты домена явно выражены с использованием вездесущего языка, простота расширения, логика валидации централизована и валидаторы может использоваться вместе для обеспечения соблюдения сложных правил домена. И даже когда я знаю, что размещаю проверку своих сущностей вне их (Вы могли бы поспорить, что запах кода - Anemic Domain), но я думаю, что компромисс приемлем

Но есть одна вещь, которую я так и не понял, как правильно ее реализовать.How should I use this components...

Поскольку они будут внедрены, они не будут естественным образом вписываться в сущности моего домена, поэтому в основном я вижу два варианта:

  1. Pass the validators to each method of my entity

  2. Validate my objects externally (from the command handler)

Я не доволен вариантом 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();
      }
   }
}

Ну вот и все. Можете ли вы поделиться своими мыслями по этому поводу или поделиться своим опытом проверки доменных сущностей?

EDIT

Я думаю, что это не ясно из моего вопроса, но реальная проблема заключается в следующем: сокрытие правил домена имеет серьезные последствия для будущей ремонтопригодности приложения, а также правила домена часто меняются в течение жизненного цикла приложения. Следовательно, реализация их с учетом этого позволит нам легко их расширить. Теперь представьте, что в будущем будет реализован механизм правил, если правила инкапсулируются вне сущностей домена, это изменение будет легче реализовать

Я знаю, что размещение проверки вне моих сущностей нарушает инкапсуляцию, как @jgauffin упомянул в своем ответе, но я думаю, что преимущества размещения валидации в отдельных объектах гораздо более существенны, чем просто сохранение инкапсуляции сущности. Теперь я думаю, что инкапсуляция имеет больше смысла в традиционной n-уровневой архитектуре, потому что объекты использовались в нескольких местах уровня домена, но в архитектуре CQRS, когда команда прибудет, будет обработчик команд, обращающийся к совокупному корню и выполнение операций с совокупным корнем, только создав идеальное окно для размещения проверки.

Я хотел бы сделать небольшое сравнение между преимуществами размещения проверки внутри объекта по сравнению с размещением его в отдельных объектах.

  • Validation in Individual objects

    • Pro. Easy to write
    • Pro. Easy to test
    • Pro. It's explicitly expressed
    • Pro. It becomes part of the Domain design, expressed with the current Ubiquitous Language
    • Pro. Since it's now part of the design, it can be modeled using UML diagrams
    • Pro. Extremely easy to maintain
    • Pro. Makes my entities and the validation logic loosely coupled
    • Pro. Easy to extend
    • Pro. Following the SRP
    • Pro. Following the Open/Close principle
    • Pro. Not breaking the law of Demeter (mmm)?
    • Pro. I'is centralized
    • Pro. It could be reusable
    • Pro. If required, external dependencies can be easily injected
    • Pro. If using a plug-in model, new validators can be added just by dropping the new assemblies without the need to re-compile the whole application
    • Pro. Implementing a rules engine would be easier
    • Con. Breaking encapsulation
    • Con. If encapsulation is mandatory, we would have to pass the individual validators to the entity (aggregate) method
  • Validation encapsulated inside the entity

    • Pro. Encapsulated?
    • Pro. Reusable?

Я хотел бы прочитать ваши мысли по этому поводу

Большинство предметов, перечисленных как «плюсы» для размещения логики проверки в отдельных объектах также могут быть перечислены как «за» для размещения логики проверки в сущности. eulerfx

Ваш Ответ

11   ответов
2

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

Я бы не стал переносить логику проверки электронной почты изCustomer к чему-либо еще, чтобы следовать принципу Open / Closed. Для меня, после открытия / закрытия будет означать, что у вас есть следующая иерархия:

public class User
{
    // some basic validation
    public virtual void ChangeEmail(string email);
}

public class Employee : User
{
    // validates internal email
    public override void ChangeEmail(string email);
}

public class Customer : User
{
    // validate external email addresses.
    public override void ChangeEmail(string email);
}

Вы предлагаете переместить элемент управления из модели предметной области в произвольный класс, тем самым нарушая инкапсуляцию. Я бы предпочел рефакторинг своего класса (Customer) соблюдать новые бизнес-правила, чем делать это.

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

Exceptions

Я просто заметил, что вы бросаетеDomainException, Это способ общего исключения. Почему вы не используете исключения аргумента илиFormatException? Они описывают ошибку намного лучше. И не забудьте включить контекстную информацию, которая поможет вам предотвратить исключение в будущем.

Update

Размещение логики вне класса вызывает проблемы imho. Как вы контролируете, какое правило проверки используется? Одна часть кода может использоватьSomeVeryOldRule при проверке в то время как другое использованиеNewAndVeryStrictRule, Это может быть не нарочно, но может и произойдет, когда кодовая база будет расти.

Похоже, вы уже решили игнорировать одну из основ ООП (инкапсуляция). Продолжайте и используйте общую / внешнюю структуру проверки, но не говорите, что я вас не предупреждал;)

Update2

Thanks for your patience and your answers, and that's the reason why I posted this question, I feel the same an entity should be responsible to guarantee it's in a valid state (and I have done it in previous projects) but the benefits of placing it in individual objects is huge and like I posted there's even a way to use individual objects and keep the encapsulation but personally I am not so happy with design but on the other hand it is not out of the table, consider this ChangeEmail(IEnumerable> validators, string email) I have not thought in detail the imple. though

Это позволяет программисту указывать любые правила, это может быть или не быть в настоящее время правильными бизнес-правилами. Разработчик мог просто написать

customer.ChangeEmail(new IValidator<Customer>[] { new NonValidatingRule<Customer>() }, "notAnEmail")

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

Если вы хотите использовать механизм правил, создайте одноэлементный прокси:

public class Validator
{
    IValidatorEngine _engine;

    public static void Assign(IValidatorEngine engine)
    {
        _engine = engine;
    }

    public static IValidatorEngine Current { get { return _engine; } }
}

.. и использовать его из методов модели предметной области, таких как

public class Customer
{
    public void ChangeEmail(string email)
    {
        var rules = Validator.GetRulesFor<Customer>("ChangeEmail");
        rules.Validate(email);

        // valid
    }

}

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

Решение является более гибким, но для его реализации потребуется гораздо больше времени, чем для реорганизации метода, бизнес-правила которого были изменены.

DomainException только в качестве примера, есть несколько конкретных исключений Jupaol
Вы не поняли мою точку зрения. DataAnnotations и т. Д., Созданные по той же причине. Ноnothing кроме самого класса, можно гарантировать, что он находится в допустимом состоянии. Если вы думаете, что это жертва, на которую вы готовы пойти, тогда непременно: продолжайте.
Я думаю, что это не ясно из моего вопроса, но реальная проблема заключается в следующем: сокрытие правил домена имеет серьезные последствия для будущей ремонтопригодности приложения, а также правила домена часто меняются в течение жизненного цикла приложения. Следовательно, реализация их с учетом этого позволит нам легко их расширить. Теперь представьте, что в будущем будет реализован механизм правил, если правила, инкапсулированные вне сущностей домена, облегчат это изменение. Jupaol
Но почему вы должны знать бизнес-правила электронной почты вне метода? Пользователь, который вводит электронное письмо, должен получить подсказку из сообщений об исключениях о том, что он ввел неправильно. (и я сказал, что ваш первый пример кода не нарушает OCP, за исключением того, что он не имеетvirtual ключевое слово)
EntityBase не имеет ничего общего с упорством. Это POCO, добавляющая общую логику в GetHashCode, и в основном равно. Я не согласен с тем, как вы моделируете принцип Open / Close. Но я полностью согласен с вами в том, что логикаCustomer нарушает инкапсуляцию (я говорил об этом в посте), и я не доволен этим, но компромиссы против размещения его снаружи больше и важнее, с моей точки зрения, как вы думаете? Другой подход, который я мог бы использовать, - передать объекты валидатора моим методам сущностей примерно так:ChangeEmail(IEnumerable<IValidator> v) Jupaol
2

что то, что я сделал, является идеальной вещью, потому что я все еще борюсь с этой проблемой сам и сражаюсь по одному бою за раз. Но я до сих пор делал следующее:

У меня есть базовые классы для инкапсуляции проверки:

public interface ISpecification<TEntity> where TEntity : class, IAggregate
    {
        bool IsSatisfiedBy(TEntity entity);
    }

internal class AndSpecification<TEntity> : ISpecification<TEntity> where TEntity: class, IAggregate
    {
        private ISpecification<TEntity> Spec1;
        private ISpecification<TEntity> Spec2;

        internal AndSpecification(ISpecification<TEntity> s1, ISpecification<TEntity> s2)
        {
            Spec1 = s1;
            Spec2 = s2;
        }

        public bool IsSatisfiedBy(TEntity candidate)
        {
            return Spec1.IsSatisfiedBy(candidate) && Spec2.IsSatisfiedBy(candidate);
        }


    }

    internal class OrSpecification<TEntity> : ISpecification<TEntity> where TEntity : class, IAggregate
    {
        private ISpecification<TEntity> Spec1;
        private ISpecification<TEntity> Spec2;

        internal OrSpecification(ISpecification<TEntity> s1, ISpecification<TEntity> s2)
        {
            Spec1 = s1;
            Spec2 = s2;
        }

        public bool IsSatisfiedBy(TEntity candidate)
        {
            return Spec1.IsSatisfiedBy(candidate) || Spec2.IsSatisfiedBy(candidate);
        }
    }

    internal class NotSpecification<TEntity> : ISpecification<TEntity> where TEntity : class, IAggregate
    {
        private ISpecification<TEntity> Wrapped;

        internal NotSpecification(ISpecification<TEntity> x)
        {
            Wrapped = x;
        }

        public bool IsSatisfiedBy(TEntity candidate)
        {
            return !Wrapped.IsSatisfiedBy(candidate);
        }
    }

    public static class SpecsExtensionMethods
    {
        public static ISpecification<TEntity> And<TEntity>(this ISpecification<TEntity> s1, ISpecification<TEntity> s2) where TEntity : class, IAggregate
        {
            return new AndSpecification<TEntity>(s1, s2);
        }

        public static ISpecification<TEntity> Or<TEntity>(this ISpecification<TEntity> s1, ISpecification<TEntity> s2) where TEntity : class, IAggregate
        {
            return new OrSpecification<TEntity>(s1, s2);
        }

        public static ISpecification<TEntity> Not<TEntity>(this ISpecification<TEntity> s) where TEntity : class, IAggregate
        {
            return new NotSpecification<TEntity>(s);
        }
    }

и чтобы использовать это, я делаю следующее:

обработчик команд:

 public class MyCommandHandler :  CommandHandler<MyCommand>
{
  public override CommandValidation Execute(MyCommand cmd)
        {
            Contract.Requires<ArgumentNullException>(cmd != null);

           var existingAR= Repository.GetById<MyAggregate>(cmd.Id);

            if (existingIntervento.IsNull())
                throw new HandlerForDomainEventNotFoundException();

            existingIntervento.DoStuff(cmd.Id
                                , cmd.Date
                                ...
                                );


            Repository.Save(existingIntervento, cmd.GetCommitId());

            return existingIntervento.CommandValidationMessages;
        }

совокупность:

 public void DoStuff(Guid id, DateTime dateX,DateTime start, DateTime end, ...)
        {
            var is_date_valid = new Is_dateX_valid(dateX);
            var has_start_date_greater_than_end_date = new Has_start_date_greater_than_end_date(start, end);

        ISpecification<MyAggregate> specs = is_date_valid .And(has_start_date_greater_than_end_date );

        if (specs.IsSatisfiedBy(this))
        {
            var evt = new AgregateStuffed()
            {
                Id = id
                , DateX = dateX

                , End = end        
                , Start = start
                , ...
            };
            RaiseEvent(evt);
        }
    }

спецификация теперь встроена в эти два класса:

public class Is_dateX_valid : ISpecification<MyAggregate>
    {
        private readonly DateTime _dateX;

        public Is_data_consuntivazione_valid(DateTime dateX)
        {
            Contract.Requires<ArgumentNullException>(dateX== DateTime.MinValue);

            _dateX= dateX;
        }

        public bool IsSatisfiedBy(MyAggregate i)
        {
            if (_dateX> DateTime.Now)
            {
                i.CommandValidationMessages.Add(new ValidationMessage("datex greater than now"));
                return false;
            }

            return true;
        }
    }

    public class Has_start_date_greater_than_end_date : ISpecification<MyAggregate>
    {
        private readonly DateTime _start;
        private readonly DateTime _end;

        public Has_start_date_greater_than_end_date(DateTime start, DateTime end)
        {
            Contract.Requires<ArgumentNullException>(start == DateTime.MinValue);
            Contract.Requires<ArgumentNullException>(start == DateTime.MinValue);

            _start = start;
            _end = end;
        }

        public bool IsSatisfiedBy(MyAggregate i)
        {
            if (_start > _end)
            {
                i.CommandValidationMessages.Add(new ValidationMessage(start date greater then end date"));
                return false;
            }

            return true;
        }
    }

Это позволяет мне повторно использовать некоторые проверки для разных агрегатов, и это легко проверить. Если вы видите какие-либо потоки в нем. Я был бы очень рад это обсудить.

твое,

До сих пор я не тестировал в отдельности мой метод совокупного корня. Это недостаток, который я обнаруживаю, проходя через «обсуждение». Вы имели с @jgauffin. У меня есть класс для метода IsSatisfiedBy (MyAggregate i), поэтому, если мне нужны внешние зависимости. Это будет происходить от инъекции Ctor. На самом деле, по крайней мере для меня, даже аргументы, исходящие от команды, являются внешними зависимостями. Но я могу измениться в будущем. Я определенно буду изучать эту инъекцию проверки, чтобы отделить мой сводный корневой метод от проверки. Хорошего дня...
Похоже, ваш процесс - это почти тот процесс, который я представил, что заставляет меня думать, что я не одинок в этой тенденции, единственное отличие состоит в том, что вы инкапсулировали правила внутри метода, который на первый взгляд выглядит хорошо, но у меня есть к вам вопрос Вы интенсивно пишете модульные тесты для своего кода? потому что, поскольку вы создаете объекты внутри, вы не можете изолированно тестировать метод совокупного корня. В качестве еще одного побочного эффекта, если для валидатора требуется внешняя зависимость, вам придется использовать антишаблон Service Locator и проверить, что в итоге вы получите интеграционные тесты. Jupaol
Ваша реализация шаблона спецификации фактически решает проблему,mutate first the entity in order to validate it Я буду оценивать этот вид дизайна. Я не уверен, что буду использовать конструктор для передачи значений, потому что я хотел иметь гибкий дизайн, в который я мог бы вводить зависимости для валидаторов. Проверить эту ссылку на основе Misko Heveryloosecouplings.com/2011/01/… Jupaol
5

и я собираюсь реализовать мою проверку за пределами моих доменных сущностей. Объекты моего домена будут содержать логику для защиты любых инвариантов (таких как отсутствующие аргументы, нулевые значения, пустые строки, коллекции и т. Д.). Но реальные бизнес-правила будут жить в классах валидаторов. Я настроен на @SonOfPirate ...

я используюFluentValidation по сути, это даст мне кучу валидаторов, действующих на мои доменные сущности: иначе говоря, шаблон спецификации. Кроме того, в соответствии с шаблонами, описанными в синей книге Эрика, я могу создавать валидаторы с любыми данными, которые могут им понадобиться для выполнения валидаций (будь то из базы данных или другого хранилища или службы). Я также мог бы добавить сюда любые зависимости. Я также могу составить и повторно использовать эти валидаторы (например, валидатор адреса может быть повторно использован как в валидаторе сотрудника, так и в валидаторе компании). У меня есть фабрика Validator, которая действует как «сервисный локатор»:

public class ParticipantService : IParticipantService
{
    public void Save(Participant participant)
    {
        IValidator<Participant> validator = _validatorFactory.GetValidator<Participant>();
        var results = validator.Validate(participant);
            //if the participant is valid, register the participant with the unit of work
            if (results.IsValid)
            {
                if (participant.IsNew)
                {
                    _unitOfWork.RegisterNew<Participant>(participant);
                }
                else if (participant.HasChanged)
                {
                    _unitOfWork.RegisterDirty<Participant>(participant);
                }
            }
            else
            {
                _unitOfWork.RollBack();
                //do some thing here to indicate the errors:generate an exception (or fault) that contains the validation errors. Or return the results
            }
    }

}

И валидатор будет содержать код, что-то вроде этого:

   public class ParticipantValidator : AbstractValidator<Participant>
    {
        public ParticipantValidator(DateTime today, int ageLimit, List<string> validCompanyCodes, /*any other stuff you need*/)
        {...}

    public void BuildRules()
    {
             RuleFor(participant => participant.DateOfBirth)
                    .NotNull()
                    .LessThan(m_today.AddYears(m_ageLimit*-1))
                    .WithMessage(string.Format("Participant must be older than {0} years of age.", m_ageLimit));

            RuleFor(participant => participant.Address)
                .NotNull()
                .SetValidator(new AddressValidator());

            RuleFor(participant => participant.Email)
                .NotEmpty()
                .EmailAddress();
            ...
}

    }

Мы должны поддерживать несколько типов презентаций: веб-сайты, winforms и массовую загрузку данных через сервисы. Под закреплением всего этого понимается набор сервисов, которые раскрывают функциональность системы единым и единообразным способом. Мы не используем Entity Framework или ORM по причинам, по которым я не буду утомлять вас.

Вот почему мне нравится этот подход:

The business rules that are contained in the validators are totally unit testable. I can compose more complex rules from simpler rules I can use the validators in more than one location in my system (we support websites and Winforms, and services that expose functionality), so if there is a slightly different rule required for a use case in a service that differs from the websites, then I can handle that. All the vaildation is expressed in one location and I can choose how / where to inject and compose this.
При таком подходе было бы много пар? Скажите EmailAddressValidator с помощью EmailAddressValueObject? А как насчет имен? Может быть, идти с именем первого агрегата, в котором он использовался (т.е. Users \ EmailAddressValidator)? Если так, то я думаю, что валидаторы и объекты-ценности выигрывают от объединения. Вы можете иметь объекты общих значений и объекты значений, специфичные для конкретного агрегата. Я просто думаю здесь ... как я новичок в DDD.
-3

как объясненоВот.

Исключения не являются правильным методом для всех ошибок проверки, не говорится, что недействительный объект является исключительным случаем.

Если проверка не является тривиальной, логику для проверки совокупности можно выполнить непосредственно наserver и пока вы пытаетесь установить новый ввод, вы можете инициировать событие домена, чтобы сообщитьuser (или приложение, которое использует ваш домен), почему ввод неправильный.

По словам Мартина Фаулера"The essence of a Domain Event is that you use it to capture things that can trigger a change to the state of the application you are developing. These event objects are then processed to cause changes to the system, and stored to provide an Audit Log", Я действительно использую доменные события в этом контексте Jupaol
Согласно википедии & quot;anomalous or exceptional situations requiring special processing – often changing the normal flow of program execution& Quot ;. Кто-то говорит, что когда вы используете словоwhen, там, есть событие домена. Так что, если эксперт по домену говорит: & quot;when the customer email is not valid do...& Quot; есть доменное событие.
Доменная сущность всегда должна быть в согласованном состоянии. Таким образом, попытка указать недействительные данные является исключительной.
Исключения следует использовать, только если вы не знаете, как решить проблему в определенном контексте, ошибка проверки не имеет место. Если электронная почта неверна, да, проверка на уровне представления и / или сервисных уровней не сработала, но пользователю легко повторно попросить ввести действительный адрес, чтобы рабочий процесс не был скомпрометирован.
Согласовано, но это роль валидации пользовательского интерфейса и затрагивает тот факт, что в каждом слое должна быть валидация, ориентированная на то, для каких целей валидация служит. Например, проверка пользовательского интерфейса используется для обеспечения интерактивности и (надеюсь) предотвращения передачи неверных данных в домен. На уровне домена проверка используется для предотвращения входа недопустимых состояний в объекты домена. Я согласен, что исключения уместны на уровне домена. Если вы не хотите, чтобы возникла исключительная ситуация, выполните проверку пользовательского интерфейса, чтобы помочь «пользователю». предоставить правильные данные.
0

ылка состояла в том, что существуют разные виды валидации. Я назвал их поверхностной проверкой и проверкой команд на основе домена.

Эта простая версия такова. Подтверждение таких вещей, как «это число» или «адрес электронной почты»; чаще, чем не просто поверхностно. Это можно сделать до того, как команда достигнет сущностей домена.

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

Тогда у вас есть гибридные типы. Такие вещи, как проверка на основе набора. Это должно произойти до того, как команда будет введена или введена в домен (постарайтесь избежать этого, если это вообще возможно - ограничение зависимостей - это хорошо).

В любом случае, вы можете прочитать полный пост здесь:Как проверить команды в приложении CQRS

5

Вы должны использовать ValueObjects для таких вещей. Смотреть эту презентациюhttp://www.infoq.com/presentations/Value-Objects-Dan-Bergh-Johnsson Он также научит вас данным как центрам гравитации.

Там также пример того, как повторно использовать проверку данных, как, например, с использованием статических методов проверки ала Email.IsValid (строка)

Я согласен, это может предотвратить дублирование кода с моей точки зрения.
спасибо большое Евгений. Как начинающий DDD, ваши ответы всегда вдохновляют.
You put validation in the wrong place Я полностью уважаю ваше мнение, и если бы вы могли указать мне конкретные преимущества (компромиссы) между размещением проверки внутри объекта значения по сравнению с размещением ее в отдельных объектах (как я это делал в конце своего поста), я был бы признателен за это. Jupaol
1

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

Если вы не хотите возвращать правила к более низкому уровню абстракции, например, к сущности или объекту значения электронной почты, я настоятельно рекомендую вам уменьшить боль путем группировки правил. Итак, в вашем примере электронной почты следующие 3 правила:

  if(string.IsNullOrWhitespace(email))   throw new DomainException(“...”);
  if(!email.IsEmail())  throw new DomainException();
  if(email.Contains(“@mailinator.com”))  throw new DomainException();

может быть частьюEmailValidationRule группа, которую вы можете использовать повторно проще.

С моей точки зрения, нет четкого ответа на вопрос, где поставить логику валидации. Это может быть частью любого объекта в зависимости от уровня абстракции. В вашем текущем случае формальная проверка адреса электронной почты может быть частьюEmailValueObject иmailinator Правило может быть частью концепции более высокого уровня абстракции, в которой вы утверждаете, что ваш пользователь не может иметь адрес электронной почты, указывающий на этот домен. Например, если кто-то хочет связаться с вашим пользователем без регистрации, вы можете проверить ее электронную почту на предмет формальной проверки, но вам не нужно проверять ее электронную почту на соответствиеmailinator править. И так далее...

Поэтому я полностью согласен с @pjvds, который утверждал, что этот вид неудобной проверки является признаком плохого дизайна. Я не думаю, что вы выиграете, нарушив инкапсуляцию, но это ваш выбор, и это будет ваша боль.

0

а не сущности (или совокупного корня).

Я бы разделил валидацию на отдельные области.

Validate internal characteristics of the Email value object internally.

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

использованиеcreateNew() создать экземпляр электронного письма из пользовательского ввода. Это заставляет его быть действительным в соответствии с вашими текущими правилами (например, формат & quot; [email protected]").

использованиеcreateExisting() создать экземпляр электронной почты из постоянного хранилища. Это не выполняет проверку, что важно - вы не хотите, чтобы было выброшено исключение для сохраненного электронного письма, которое было действительным вчера, но недействительным сегодня.

class Email
{
    private String value_;

    // Error codes
    const Error E_LENGTH = "An email address must be at least 3 characters long.";
    const Error E_FORMAT = "An email address must be in the '[email protected]' format.";

    // Private constructor, forcing the use of factory functions
    private Email(String value)
    {
        this.value_ = value;
    }

    // Factory functions
    static public Email createNew(String value)
    {
        validateLength(value, E_LENGTH);
        validateFormat(value, E_FORMAT);
    }

    static public Email createExisting(String value)
    {
        return new Email(value);
    }

    // Static validation methods
    static public void validateLength(String value, Error error = E_LENGTH)
    {
        if (value.length() < 3)
        {
            throw new DomainException(error);
        }
    }

    static public void validateFormat(String value, Error error = E_FORMAT)
    {
        if (/* regular expression fails */)
        {
            throw new DomainException(error);
        }
    }

}

Validate "external" characteristics of the Email value object externally, e.g., in a service.

class EmailDnsValidator implements IEmailValidator
{
    const E_MX_MISSING = "The domain of your email address does not have an MX record.";

    private DnsProvider dnsProvider_;

    EmailDnsValidator(DnsProvider dnsProvider)
    {
        dnsProvider_ = dnsProvider;
    }

    public void validate(String value, Error error = E_MX_MISSING)
    {
        if (!dnsProvider_.hasMxRecord(/* domain part of email address */))
        {
            throw new DomainException(error);
        }
    }
}

class EmailDomainBlacklistValidator implements IEmailValidator
{
    const Error E_DOMAIN_FORBIDDEN = "The domain of your email address is blacklisted.";

    public void validate(String value, Error error = E_DOMAIN_FORBIDDEN)
    {
        if (/* domain of value is on the blacklist */))
        {
            throw new DomainException(error);
        }
    }
}

Преимущества:

Use of the createNew() and createExisting() factory functions allow control over internal validation.

It is possible to "opt out" of certain validation routines, e.g., skip the length check, using the validation methods directly.

It is also possible to "opt out" of external validation (DNS MX records and domain blacklisting). E.g., a project I worked on initially validated the existance of MX records for a domain, but eventually removed this because of the number of customers using "dynamic IP" type solutions.

It is easy to query your persistent store for email addresses that do not fit the current validation rules, but running a simple query and treating each email as "new" rather than "existing" - if an exception is thrown, there's a problem. From there you can issue, for example, a FlagCustomerAsHavingABadEmail command, using the exception error message as guidance for the user when they see the message.

Allowing the programmer to supply the error code provides flexibility. For example, when sending a UpdateEmailAddress command, the error of "Your email address must be at least 3 characters long" is self explanatory. However, when updating multiple email addresses (home and work), the above error message does not indicate WHICH email was wrong. Supplying the error code/message allows you to provide richer feedback to the end user.

0

но вы можете попробовать Decorators. Если вы используете SimpleInjector, вы можете легко внедрить свои собственные классы проверки, которые выполняются перед вашим обработчиком команд. Тогда команда может предположить, что она действительна, если она зашла так далеко. Однако это означает, что вся проверка должна выполняться по команде, а не по объектам. Объекты не перейдут в недопустимое состояние. Но каждая команда должна полностью реализовывать свою собственную проверку, чтобы похожие команды могли иметь дублирование правил, но вы можете либо абстрагировать общие правила для совместного использования, либо рассматривать разные команды как действительно отдельные.

7

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

Почему бы не сделатьValueObject называетсяEmail что делает эта проверка в конструкции?

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

Что касается объекта значения, это имеет смысл для меня, я бы инкапсулировал валидацию, где он действительно принадлежит, а не в сущности, но я думаю, что проблемы такие же, как и размещение его в сущности, я имею в виду преимущества, перечисленные в пост, они одинаковы либо проверка находится внутри объекта или объекта значения. В частности, каковы будут компромиссы при использовании валидации внутри объекта значения над компромиссами их размещения в отдельных объектах ??? Jupaol
I wouldn't suggest trowing big pieces of code Но валидаторы действительно маленькие и конкретные, и они следуют SRP.but I prefer value object because you make the related concept part of your domain Валидаторы могут рассматриваться как часть домена, и они могут моделироваться в домене, и, по моему мнению, они делают проверку явной, когда вы помещаете проверку в сущность или объект значения, проверка скрыта (действительно инкапсулирована). Jupaol
& quot; Мой опыт показывает, что неудобные проверки являются подсказками для пропущенных концепций в вашем домене. & Quot; - Я не уверен в этом правиле. По текущему сценарию он работает для перемещения кода проверки низкого уровня в объект значения низкого уровня. Более высокий код проверки уровня абстракции может не работать. Мне нужно больше опыта, но спасибо за совет.
11

представленных в других ответах, но я собрал их в своем коде.

Во-первых, я согласен с тем, что использование Value Objects для значений, которые включают поведение, является отличным способом инкапсуляции общих бизнес-правил, а адрес электронной почты - идеальный кандидат. Однако я склонен ограничивать это правилами, которые являются постоянными и не будут часто меняться. Я уверен, что вы ищете более общий подход, и электронная почта является лишь примером, поэтому я не буду фокусироваться на этом единственном сценарии использования.

Ключом к моему подходу является признание того, что валидация служит разным целям в разных местах приложения. Проще говоря, проверьте только то, что требуется, чтобы гарантировать, что текущая операция может выполняться без неожиданных / непреднамеренных результатов. Это приводит к вопросу о том, что проверка должна произойти, где?

В вашем примере я хотел бы спросить себя, действительно ли доменная сущность заботится о том, чтобы адрес электронной почты соответствовал некоторому шаблону и другим правилам, или нам просто нужно, чтобы это "электронная почта"; не может быть пустым или пустым при вызове ChangeEmail? Если последнее, то простая проверка на предмет наличия значения - это все, что нужно в методе ChangeEmail.

В CQRS все изменения, которые изменяют состояние приложения, происходят как команды с реализацией в обработчиках команд (как вы показали). Я обычно помещаю любые «крючки» в бизнес-правила и т. д., которые подтверждают, что операция МОЖЕТ выполняться в обработчике команд. Я на самом деле следую вашему подходу внедрения валидаторов в обработчик команд, который позволяет мне расширять / заменять набор правил без внесения изменений в обработчик. Эти «динамические» Правила позволяют мне определить бизнес-правила, например, что представляет собой действительный адрес электронной почты, прежде чем я изменю состояние объекта, что дополнительно гарантирует, что оно не перейдет в недопустимое состояние. Но «недействительность» в этом случае определяется бизнес-логикой и, как вы указали, является очень изменчивым.

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

Я обнаружил, что эти нюансы очень важны для ясности моего мнения по этому вопросу. Существует проверка для предотвращения неверных данных (например, отсутствующих аргументов, пустых значений, пустых строк и т. Д.), Которые принадлежат самому методу, и существует проверка для обеспечения соблюдения бизнес-правил. В первом случае, если у Клиента должен быть адрес электронной почты, единственное правило, о котором я должен беспокоиться, чтобы не допустить недействительности объекта моего домена, состоит в том, чтобы адрес электронной почты был предоставлен Метод ChangeEmail. Другие правила относятся к вопросам более высокого уровня относительно действительности самого значения и на самом деле не влияют на действительность самого объекта домена.

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

Наконец, есть также место для проверки пользовательского интерфейса (и под пользовательским интерфейсом я подразумеваю то, что служит интерфейсом для приложения, будь то экран, конечная точка службы или что-то еще). Я считаю вполне разумным дублировать некоторую логику в пользовательском интерфейсе, чтобы обеспечить лучшую интерактивность для пользователя. Но это потому, что эта проверка служит той единственной цели, поэтому я допускаю такое дублирование. Однако использование внедренных объектов валидатора / спецификации способствует повторному использованию таким образом без негативных последствий определения этих правил в нескольких местах.

Не уверен, помогает ли это или нет ...

"Однако я склонен ограничивать это правилами, которые являются постоянными и не будут часто меняться." Я отвечаю, что у вас могут быть объекты-значения, которые являются локальными только для вашего агрегата. И у вас могут быть объекты-значения, которые являются «общими» (поместите их в другую папку вне агрегата для использования в любом объекте). Я полагаю, что Эванс или Вон предлагают это. Вы можете начать с использования общего, пока не найдете сущность, которая нуждается в других правилах проверки.
& Quot; Другие правила относятся к вопросам более высокого уровня относительно действительности самого значения и на самом деле не влияют на действительность самой сущности домена. & Quot; - Почему это проблемы более высокого уровня? Разве мы не можем создать объект значения из адреса электронной почты и проверить правильность введенных данных с помощью установщика? Таким образом, мы можем гарантировать, что каждый адрес электронной почты проверяется на соответствие шаблону. Проверка по шаблонам электронной почты на более высоком уровне абстракции может привести к повторению кода, что может привести к ошибкам. Например, я могу забыть добавить валидатор шаблона электронной почты в сервис и т. Д.

Похожие вопросы