就像存储库抽象一样?
好吧,即使您通过声明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 细节使其适用于您的域并且这样更实用的事实,那么我真的无法判断它是否会在长期内弊大于利跑,但我不会。