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

Posted

技术标签:

【中文标题】我应该从域层抽象验证框架吗?【英文标题】:Should I abstract the validation framework from Domain layer? 【发布时间】:2015-04-08 07:56:45 【问题描述】:

我正在使用 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

始终有效的实体

也许你的设计有一个更根本的问题,如果你坚持那个学派,你甚至不会问这个问题:始终有效的实体。

从这个角度来看,不变的执行是域实体本身的责任,因此甚至不应该能够在没有有效的情况下存在。因此,不变规则被简单地表达为契约,当它们被违反时抛出异常。

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

让我们提议我们现在有一个SendUserCreationEmailService,它需要一个 UserProfile ...我们如何才能使Name 的服务合理化 不是null?我们再检查一遍吗?或者更有可能......你只是不 费心检查并“希望最好”你希望有人打扰 在将其发送给您之前对其进行验证。当然使用 TDD 之一 我们应该编写的第一个测试是,如果我向客户发送 null 应该引发错误的名称。但是一旦我们开始写作 我们一遍又一遍地意识到这些测试......“等一下,如果我们 绝不允许 name 变为 null 我们不会进行所有这些测试”- Greg Young 评论 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 的位置。如果你仍然不相信,问问自己你将如何整合价值对象?每次实例化一个Username 值对象时,你会有一个UsernameValidator 来验证它吗?显然,这没有任何意义,并且值对象的使用将很难与非始终有效的方法集成。

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

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

基本上,我的理解是收集和返回错误不是域的工作,这是一个 UI 问题。如果无效数据进入域,它只会扔给你。

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

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

另外,如果你仍然担心 DRY,有人曾经告诉我代码重用也是“耦合”,我认为这一事实在这里尤为重要。

处理域中的延迟验证

我不会在这里重新解释这些,但是有多种方法可以处理域中的延迟验证,例如规范模式和 Ward Cunningham 在他的 Checks 模式语言中描述的 Deferred Validation 方法。如果您有 Vaughn Vernon 撰写的实施领域驱动设计一书,还可以阅读第 208-215 页。

这始终是一个权衡的问题

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

因此,如果您认为可以接受一些 FluentValidation 细节使其适用于您的域并且这样更实用的事实,那么我真的无法判断它是否会在长期内弊大于利跑,但我不会。

【讨论】:

@plalx,很好的答案。我在网上看到很多在域模型中使用 FluentValidation 的示例。域模型知道它的验证器并负责测试自己并在出现任何问题时抛出异常。它实际上只是从域对象到验证器的验证逻辑的提​​取(并且可能共享一些验证)。但如果我理解正确,你认为这不值得麻烦吗? @Tenek 不是提取规则本身很麻烦(在某些情况下甚至可能是可取的),而是域对第三方库的依赖,其中大多数时间弊大于利。当您必须收集并同时显示许多错误时,验证框架最有帮助,而在域中您一出现问题就停下来,因为目标是保护数据的完整性,而不是引导用户完成一个过程并解释出了什么问题。【参考方案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();
    

请注意,我还使用 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();
    

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

希望对我有所帮助:)

编辑:

由于一些程序员不喜欢使用“服务定位器反模式”,这里有一个非常简单的示例,说明如何删除它并仍然开心:)

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

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

【讨论】:

从我读到的内容中,您不应该在代码中的任何地方使用 IOC 容器,而是在组合根或可能的***别。否则,即使在注入定位器时,它仍然非常类似于服务定位器模式,因为您仍在隐藏依赖项。您无法从课堂外告诉容器将询问什么。更糟糕的是,如果在实例化对象时没有正确配置容器,您可能会遇到运行时错误。 @plalx 我从不同意这种观点,因为除了基础设施层之外,我没有在任何地方公开 IOC 容器抽象。我用它作为一个非常强大的工具。但您可以随意颠倒范式,只需将验证器注册到抽象内部的集合中即可。 为了快速,你的领域类库应该只包含实体模型,唯一的依赖应该是抽象。只有在您的核心应用程序(例如服务总线或 Web API)上,您才应该将它们放在一起。这样你就会有像 Myproject.Domain、MyProject.Validation.FluentValidation 和最后的 MyProject.Validation.FluentValidation.Autofac。 @RagingKore 如果您有兴趣,我对这个主题发表了看法。 我刚刚发现 Greg 也回答了这个问题...codebetter.com/gregyoung/2009/05/22/always-valid【参考方案3】:

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

【讨论】:

感谢您的回复@Василий Хатунцев。我只是在进行业务规则验证。

以上是关于我应该从域层抽象验证框架吗?的主要内容,如果未能解决你的问题,请参考以下文章

要使用 SmartCard 身份验证制作 React 应用程序,我应该使用 pkcs11 吗?

每当我通过 ajax 调用它时,Django Rest 框架会用 AnonymousUser 替换我当前经过身份验证的用户吗?

将验证属性从域实体映射到 DTO

zend 框架子表单isValidPartial?

在 ASP.NET MVC 框架中验证已发布的表单数据

如何用jqueryEasyUi框架验证密码只能输入六位包含数字字母?