Подтвердить что ты не робот

Проверка домена в архитектуре CQRS

Опасность... Опасность Д-р Смит... Философский пост впереди

Цель этого сообщения заключается в том, чтобы определить, действительно ли размещение логики проверки вне моих доменных сущностей (фактический совокупный корень) предоставляет мне большую гибкость или код 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. Многоразовый?

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

4b9b3361

Ответ 1

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

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

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

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

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

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

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

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

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

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

Ответ 2

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

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

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

Ответ 3

Вы ставите проверку не в том месте.

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

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

Ответ 4

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

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

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 по причинам, из-за которых я не буду вас беспокоить.

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

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

Ответ 5

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

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

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

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

Ваш,

Ответ 6

Я бы не назвал класс, который наследует от 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), чтобы соответствовать новым бизнес-правилам, чем делать это.

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

Исключения

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

Обновление

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

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

Update2

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

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

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

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

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

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
    }

}

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

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

Ответ 7

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

Если вы не хотите переводить правила обратно на более низкий уровень абстракции, например объект объекта или объекта электронной почты, я настоятельно рекомендую вам уменьшить боль, группируя правила. Поэтому в вашем примере электронной почты приведены следующие 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, который утверждал, что такая неудобная размещенная проверка является признаком плохого дизайна. Я не думаю, что у вас будет какая-то прибыль, нарушив инкапсуляцию, но это ваш выбор, и это будет вашей болью.

Ответ 8

Валидация в вашем примере - это проверка объекта значения, а не объекта (или совокупного корня).

Я бы отделил проверку на отдельные области.

  • Проверять внутренние свойства объекта значения Email внутри.

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

Используйте createNew() для создания электронной почты с пользовательского ввода. Это заставляет его действовать в соответствии с вашими текущими правилами (например, формат "[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);
        }
    }

}
  1. Проверять "внешние" характеристики объекта значения Email извне, например, в службе.

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

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

  • Использование функций createNew() и createExisting() factory позволяет контролировать внутреннюю проверку.

  • Можно "отказаться" от некоторых подпрограмм проверки, например, пропустить проверку длины, используя методы проверки напрямую.

  • Также возможно "отказаться" от внешней проверки (записи DNS MX и черный список доменов). Например, проект, над которым я работал, сначала подтвердил существование записей MX для домена, но в итоге удалил это из-за количества клиентов, использующих решения типа "динамический IP".

  • Легко запросить ваш постоянный магазин для адресов электронной почты, которые не соответствуют текущим правилам проверки, но запускают простой запрос и обрабатывают каждое электронное письмо как "новое", а не "существующее" - если возникает исключение, есть проблема. Оттуда вы можете выдать, например, команду FlagCustomerAsHavingABadEmail, используя сообщение об ошибке исключения как указание для пользователя, когда они видят сообщение.

  • Предоставление программисту возможности предоставить код ошибки обеспечивает гибкость. Например, при отправке команды UpdateEmailAddress ошибка "Ваш адрес электронной почты должна быть длиной не менее 3 символов" является самоочевидной. Однако при обновлении нескольких адресов электронной почты (дома и работы) указанное выше сообщение об ошибке не указывает, что адрес электронной почты был неправильным. Предоставление кода ошибки/сообщения позволяет предоставить более высокую обратную связь конечному пользователю.

Ответ 9

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

Эта простая версия. Валидация таких вещей, как "это число" или "адрес электронной почты", чаще всего не поверхностна. Они могут быть выполнены до того, как команда достигнет объектов домена.

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

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

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

Ответ 10

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

Ответ 11

Вы можете использовать решение на основе сообщений с событиями домена, как описано здесь.

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

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