我应该从域层抽象验证框架吗?

Should I abstract the validation framework from Domain layer?

我正在使用 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 表示领域层必须是技术独立的。

我正在做的是使用验证框架而不是自定义异常。

将验证框架放在领域层是个坏主意吗?

如果我理解正确,我认为这样做没有任何问题,只要它被抽象为基础架构问题,就像您的回购协议抽象持久性技术一样。

例如,我为我的项目创建了一个 returns 按对象类型验证器的 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();
    }
}

请注意,我还使用 IDependencyResolver 抽象了我的 IOC 容器,以便我可以使用我想要的任何实现。 (目前使用 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();
    }
}

代码可能缺少我的一些帮助程序和扩展程序,但我相信这足以让您继续前进。

希望对您有所帮助:)

编辑:

由于一些同事不喜欢使用 "service locator anti pattern",这里有一个非常简单的示例,说明如何删除它并且仍然很高兴:)

该代码提供了一个字典 属性,应按类型填充所有验证器。

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

您的问题的答案取决于您希望将哪种验证放入验证器 class。验证可以是域模型的一部分,在您的情况下,您已经使用 FluentValidation 实现了它,我看不出有任何问题。领域模型的关键——你可以在任何地方使用你的领域模型,例如,如果你的项目包含 web 部件,api,与其他子系统的集成。每个模块都引用您的域模型并且对所有模块都一样。

就像存储库抽象一样?

好吧,即使您通过声明 IUserValidator 接口来保护您的域不受框架影响,我也发现您的设计存在一些问题。

起初,这似乎会导致与存储库和其他基础设施问题相同的抽象策略,但我认为存在巨大差异。

当使用 repository.save(...) 时,您实际上根本不关心领域角度的实现,因为如何持久化事物不是领域关注的问题。

然而,不变的强制实施是一个领域问题,您不必深入研究基础架构细节(UserValidtor 现在可以这样看)来查看它们的组成,这基本上就是您将要做的如果你沿着这条路走下去,最终会做,因为规则将在框架条款中表达,并且会存在于域之外。

为什么它会住在外面?

domain -> IUserRepository
infrastructure -> HibernateUserRepository

domain -> IUserValidator
infrastructure -> FluentUserValidator

Always-valid 个实体

也许您的设计存在更根本的问题,如果您坚持以下学派,您甚至不会问这个问题:always-valid 个实体。

从这个角度来看,不变的执行是域实体本身的责任,因此如果不有效就不能存在。因此,不变规则简单地表示为合同,当违反这些规则时会抛出异常。

这背后的原因是许多错误来自于 object 处于它们不应该处于的状态这一事实。举一个我从 Greg Young 那里读到的例子:

Let's propose we now have a SendUserCreationEmailService that takes a UserProfile ... how can we rationalize in that service that Name is not null? Do we check it again? Or more likely ... you just don't bother to check and "hope for the best" you hope that someone bothered to validate it before sending it to you. Of course using TDD one of the first tests we should be writing is that if I send a customer with a null name that it should raise an error. But once we start writing these kinds of tests over and over again we realize ... "wait if we never allowed name to become null we wouldn't have all of these tests" - Greg Young commenting on http://jeffreypalermo.com/blog/the-fallacy-of-the-always-valid-entity/

请不要误会我的意思,显然您不能以这种方式强制执行所有验证规则,因为某些规则特定于禁止该方法的某些业务操作(例如,保存实体的草稿副本),但这些规则不是't 被视为与不变强制执行相同的方式,这是适用于所有场景的规则(例如,客户必须有一个名字)。

将 always-valid 原则应用于您的代码

如果我们现在查看您的代码并尝试应用 always-valid 方法,我们会清楚地看到 UserValidator object 没有它的位置。

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 的位置。如果您仍然不相信,问问自己如何整合价值 objects?每次实例化时,您都会有一个 UsernameValidator 来验证一个 Username 值 object 吗?显然,这没有任何意义,并且值 object 的使用很难与非 always-valid 方法集成。

然后抛出异常时我们如何报告所有错误?

这实际上是我一直在努力解决的问题,我自己也问了一段时间(但我仍然不完全相信我要说的是什么)。

基本上,我了解到收集和 return 错误不是域的工作,那是 UI 的问题。如果无效数据进入域,它只会抛给你。

因此,像 FluentValidation 这样的框架将在 UI 中找到它们的天然家园,并将验证视图模型而不是领域实体。

我知道,这似乎很难接受会有一定程度的重复,但这主要是因为您可能是像我这样的 full-stack 开发人员,处理 UI 和域,而实际上这些可以而且应该被视为完全不同的项目。此外,就像视图模型和域模型一样,视图模型验证和域验证可能相似但用途不同。

此外,如果您仍然担心被 DRY,有人曾经告诉我代码重用也是 "coupling",我认为这个事实在这里尤为重要。

处理域中的延迟验证

我不会在这里 re-explain 那些,但是有多种方法可以处理域中的延迟验证,例如规范模式和 Ward Cunningham 在他的 Checks 模式中描述的 Deferred Validation 方法语言。如果您有 Vaughn Vernon 的《实施 Domain-Driven 设计》一书,您还可以阅读第 208-215 页。

一直是trade-offs

的问题

验证是一个非常困难的主题,证据是直到今天人们仍然不同意应该如何完成。有这么多因素,但最终你想要的是一个实用的、可维护的和有表现力的解决方案。你不能总是一个纯粹主义者,必须接受一些规则会被打破的事实(例如,为了使用你选择的 ORM,你可能不得不在实体中泄露一些不显眼的持久性细节)。

因此,如果您认为您可以接受 FluentValidation 的一些细节进入您的域并且它更实用这一事实,那么我真的无法判断它是否弊大于利这ong 运行 但我不会。