Должен ли я абстрагировать структуру проверки с уровня домена?

Я использую FluentValidation для проверки моих сервисных операций. Мой код выглядит так:

using FluentValidation;

IUserService
{
    void Add(User user);
}

UserService : IUserService
{
    public void Add(User user)
    {
       new UserValidator().ValidateAndThrow(user);
       userRepository.Save(user);
    }
} 

UserValidator реализует FluentValidation.AbstractValidator.

DDD говорит, что уровень домена должен быть независимым от технологии.

То, что я делаю, это использование рамки проверки вместо пользовательских исключений.

Неплохая идея разместить фреймворк проверки на уровне домена?

Ответ 1

Как абстракция репозитория?

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

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

При использовании repository.save(...) вы фактически не заботитесь о реализации с точки зрения домена, потому что то, как упорствовать, не является проблемой для домена.

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

Зачем ему жить вне?

domain -> IUserRepository
infrastructure -> HibernateUserRepository

domain -> IUserValidator
infrastructure -> FluentUserValidator

Всегда действующие объекты

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

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

Поводом для этого является то, что множество ошибок возникает из-за того, что объекты находятся в состоянии, которого они никогда не должны были быть. Чтобы показать пример, который я прочитал от Грега Янга:

Предположим теперь, что a SendUserCreationEmailService, которое принимает UserProfile... как мы можем рационализировать в этой службе, что Nameне null? Мы проверяем это снова? Или, скорее всего, вы просто не позаботиться о том, чтобы проверить и "надеяться на лучшее", вы надеетесь, что кто-то беспокоился для проверки его перед отправкой его вам. Конечно, используя TDD один из первые тесты, которые мы должны написать, это то, что если я отправлю клиента с a null, чтобы оно вызывало ошибку. Но как только мы начнем писать эти виды тестов снова и снова мы понимаем... "подождите, если мы никогда не позволяло названию становиться нулевым, у нас не было бы всех этих тестов", - комментирует Грег Янг http://jeffreypalermo.com/blog/the-fallacy-of-the-always-valid-entity/

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

Применяя всегда действующий принцип к вашему коду

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

UserService : IUserService
{
    public void Add(User user)
    {
       //We couldn't even make it that far with an invalid User
       new UserValidator().ValidateAndThrow(user);
       userRepository.Save(user);
    }
}

Поэтому в этом месте нет места для FluentValidation в домене. Если вы все еще не уверены, спросите себя, как вы интегрируете объекты ценности? Будет ли у вас UsernameValidator проверять объект значения Username каждый раз, когда он инициируется? Ясно, что это не имеет никакого смысла, и использование объектов ценности будет довольно сложно интегрировать с не всегда действительным подходом.

Как мы сообщаем обо всех ошибках, когда бросаются исключения?

Это то, с чем я боролся, и я просил себя некоторое время (и я все еще не совсем уверен в том, что я буду говорить).

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

Таким образом, фреймворки, такие как FluentValidation, найдут свой естественный дом в пользовательском интерфейсе и будут проверять модели представления, а не объекты домена.

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

Кроме того, если вы все еще беспокоитесь о том, чтобы быть СУХОЙ, кто-то однажды сказал мне, что повторное использование кода также "связано", и я думаю, что этот факт особенно важен здесь.

Работа с отсроченной валидацией в домене

Я не буду объяснять их здесь, но существуют различные подходы к рассмотрению отложенных валидаций в домене, таких как шаблон спецификации, и Отложенная проверка, описанный Уордом Каннингемом на языке шаблонов чеков. Если у вас есть Реализационная книга по дизайну, разработанная Вонном Верноном, вы также можете прочитать со страницы 208-215.

Это всегда вопрос компромиссов

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

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

Ответ 2

Если я правильно понял, я не вижу никаких проблем при этом, если он абстрагируется как инфраструктурный, так же как ваш репо ретранслирует технологию сохранения.

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

public interface IObjectValidator
{
    void Validate<T>(T instance, params string[] ruleSet);

    Task ValidateAsync<T>(T instance, params string[] ruleSet);
}

И затем я реализовал его с помощью Fluent Validation так:

public class FluentValidationObjectValidator : IObjectValidator
{
    private readonly IDependencyResolver dependencyResolver;

    public FluentValidationObjectValidator(IDependencyResolver dependencyResolver)
    {
        this.dependencyResolver = dependencyResolver;
    }

    public void Validate<T>(T instance, params string[] ruleSet)
    {
        var validator = this.dependencyResolver
            .Resolve<IValidator<T>>();

        var result = ruleSet.Length == 0
            ? validator.Validate(instance)
            : validator.Validate(instance, ruleSet: ruleSet.Join());

        if(!result.IsValid)
            throw new ValidationException(MapValidationFailures(result.Errors));
    }

    public async Task ValidateAsync<T>(T instance, params string[] ruleSet)
    {
        var validator = this.dependencyResolver
           .Resolve<IValidator<T>>();

        var result = ruleSet.Length == 0
            ? await validator.ValidateAsync(instance)
            : await validator.ValidateAsync(instance, ruleSet: ruleSet.Join());

        if(!result.IsValid)
            throw new ValidationException(MapValidationFailures(result.Errors));
    }

    private static List<ValidationFailure> MapValidationFailures(IEnumerable<FluentValidationResults.ValidationFailure> failures)
    {
        return failures
            .Select(failure =>
                new ValidationFailure(
                    failure.PropertyName, 
                    failure.ErrorMessage, 
                    failure.AttemptedValue, 
                    failure.CustomState))
            .ToList();
    }
}

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

Итак, вот какой бонусный код для autofac;)

public class FluentValidationModule : Module
{
    protected override void Load(ContainerBuilder builder)
    {
        // registers type validators
        builder.RegisterGenerics(typeof(IValidator<>));

        // registers the Object Validator and configures the Ambient Singleton container
        builder
            .Register(context =>
                    SystemValidator.SetFactory(() => new FluentValidationObjectValidator(context.Resolve<IDependencyResolver>())))
            .As<IObjectValidator>()
            .InstancePerLifetimeScope()
            .AutoActivate();
    }
}

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

Надеюсь, я помог:)

EDIT:

Поскольку некоторые коллеги-кодеры предпочитают не использовать "анти-шаблон локатора сервисов", вот очень простой пример того, как его удалить и все еще быть счастливым:)

Код предоставляет свойство словаря, которое должно заполняться всеми вашими валидаторами по типу.

public class SimpleFluentValidationObjectValidator : IObjectValidator
{
    public SimpleFluentValidationObjectValidator()
    {
        this.Validators = new Dictionary<Type, IValidator>();
    }

    public Dictionary<Type, IValidator> Validators { get; private set; }

    public void Validate<T>(T instance, params string[] ruleSet)
    {
        var validator = this.Validators[typeof(T)];

        if(ruleSet.Length > 0) // no ruleset option for this example
            throw new NotImplementedException();

        var result = validator.Validate(instance); 

        if(!result.IsValid)
            throw new ValidationException(MapValidationFailures(result.Errors));
    }

    public Task ValidateAsync<T>(T instance, params string[] ruleSet)
    {
        throw new NotImplementedException();
    }

    private static List<ValidationFailure> MapValidationFailures(IEnumerable<FluentValidationResults.ValidationFailure> failures)
    {
        return failures
            .Select(failure =>
                new ValidationFailure(
                    failure.PropertyName,
                    failure.ErrorMessage,
                    failure.AttemptedValue,
                    failure.CustomState))
            .ToList();
    }
}

Ответ 3

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