我应该从Domain层抽象validation框架吗?
我正在使用FluentValidation来validation我的服务操作。 我的代码如下所示:
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说领域层必须是技术独立的。
我正在做的是使用validation框架,而不是自定义的例外。
将validation框架放入域图层是一个坏主意?
就像存储库抽象一样?
那么,即使你通过声明一个IUserValidator
接口来保护你的域,你也可以看到你的devise有一些问题。
起初,似乎是否会导致像存储库和其他基础设施问题一样的抽象策略,但是我的观点有很大的差异。
当使用repository.save(...)
,实际上并不关心域的全部实现,因为如何保持事物不是域的问题。
然而,不变的执行是一个领域的问题,你不应该深入到基础设施的细节( UserValidtor
现在可以被看作是这样),看看它们是由什么组成,基本上,如果你沿着这条路走下去,因为这些规则将在框架条款中expression出来,而且会超出领域。
为什么会住在外面?
domain -> IUserRepository infrastructure -> HibernateUserRepository domain -> IUserValidator infrastructure -> FluentUserValidator
始终有效的实体
也许你的devise有一个更基本的问题,如果你坚持这样的学校,你甚至不会问这个问题:永远有效的实体。
从这个angular度来看,不变的执行是领域实体本身的责任,因此甚至不能无效地存在。 因此,不变规则简单地表示为契约,违反规则时抛出exception。
这背后的原因是,很多的错误来自这样的事实,即对象处于它们本来不应该存在的状态。 为了揭示我从Greg Young读到的一个例子:
让我们build议我们现在有一个
SendUserCreationEmailService
接受UserProfile
…我们如何合理化该服务的Name
不为null
? 我们再检查一次吗? 或者更有可能…你只是不打扰检查和“希望最好”,你希望有人在发送给你之前,麻烦validation它。 当然,使用TDD我们应该写的第一个testing之一是,如果我发送一个null
名称的客户,应该提出一个错误。 但是,一旦我们开始一遍又一遍地写这些testing,我们意识到…“等待,如果我们从来没有让名字变成空,我们不会有所有这些testing” – 格雷格杨评论http://jeffreypalermo.com /博客/的,谬误的最总是有效的实体/
现在不要误解我的意思,显然你不能以这种方式执行所有的validation规则,因为某些规则是特定于某些禁止这种方法的业务操作(例如保存一个实体的草稿副本),但是这些规则是不能被查看的就像不变的强制执行一样,这是在每个场景中都适用的规则(例如,一个客户必须有一个名字)。
将永远有效的原则应用于您的代码
如果我们现在看看你的代码,并尝试应用始终有效的方法,我们清楚地看到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
来validation一个Username
值对象每次它的instanciated? 显然,这没有任何意义,价值对象的使用将很难与非永久有效的方法相结合。
当抛出exception时,我们如何报告所有错误?
这实际上是我努力的一个方面,我一直在问自己一段时间(而且我还不完全相信我会说什么)。
基本上,我所了解的是收集和返回错误不是域的工作,这是UI的关注。 如果无效的数据使其到达域,它只会抛出你。
因此,像FluentValidation这样的框架将在用户界面中find自然的家园,并将validation视图模型而不是域实体。
我知道,这似乎很难接受,会有一定程度的重复,但这主要是因为你可能是一个像我这样的全栈开发人员处理UI和域,事实上,可以和应该可以查看作为完全不同的项目。 另外,就像视图模型和领域模型一样,视图模型validation和领域validation可能是相似的,但是服务于不同的目的。
另外,如果你仍然担心DRY,有人曾经告诉我,代码重用也是“耦合”,我认为这个事实在这里特别重要。
处理域中的延迟validation
我不会在这里重新解释这些,但是有很多方法来处理域中的延迟validation,例如Ward Cunningham描述的Checks模式语言中的规范模式和延迟validation方法。 如果您有Vaughn Vernon的“实施域驱动devise”一书,则还可以阅读第208-215页。
这总是一个权衡的问题
validation是一个非常困难的问题,certificate是从今天起,人们仍然不同意如何去做。 有这么多的因素,但最终你想要的是一个实用,可维护和expression的解决scheme。 你不能总是一个纯粹主义者,必须接受这样一个事实:一些规则将被打破(例如,你可能不得不泄露一个实体的一些不显眼的持久性细节,以便使用你的ORMselect)。
因此,如果您认为您可以接受一些FluentValidation细节使其适用于您的领域并且更加实用的事实,那么从长远来看,我不能确定它是否会造成更多的伤害,不会。
你的问题的答案取决于你想把什么样的validation放入validation器类。 validation可以是域模型的一部分,在你的情况下,你已经使用FluentValidation实现它,我没有看到任何问题。 领域模型的关键 – 您可以随处使用您的领域模型,例如,如果您的项目包含Web部件,API,与其他子系统的集成。 每个模块都引用您的域模型,并且适用于所有模块。
如果我正确地理解了这一点,我认为只要把它作为一个基础设施问题抽象出来,就像你的回购文件抽象出持久性技术一样。
作为一个例子,我为我的项目创build了一个IObjectValidator,它通过对象types返回validation器,以及它的一个静态实现,这样我就不再耦合到技术本身。
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(); } }
代码可能会遗漏一些我的助手和扩展,但我相信这将足以让你去。
我希望我已经帮助:)
编辑:
由于一些同行编码者不喜欢使用“服务定位反模式”,这里是一个非常简单的例子,如何删除它,仍然很高兴:)
该代码提供了一个字典属性,应该用Type来填充所有的validation器。
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(); } }