危险......危险史密斯博士......前面的哲学帖子
这篇文章的目的是确定将验证逻辑放在我的域实体之外(实际上是聚合根)是否实际上赋予了我更多的灵活性,或者它是神风敢死队代码
基本上我想知道是否有更好的方法来验证我的域实体。这就是我打算这样做的方式,但我想听听你的意见
我考虑的第一种方法是:
class Customer : EntityBase<Customer>
{
public void ChangeEmail(string email)
{
if(string.IsNullOrWhitespace(email)) throw new DomainException(“...”);
if(!email.IsEmail()) throw new DomainException();
if(email.Contains(“@mailinator.com”)) throw new DomainException();
}
}
我实际上不喜欢这种验证,因为即使我将验证逻辑封装在正确的实体中,这也违反了 Open/Close 原则(Open for extension but Close for modify),我发现违反这个原则,代码维护就变成了当应用程序变得复杂时,这是一个真正的痛苦。为什么?因为域规则的变化比我们想承认的要频繁,如果规则被隐藏并嵌入到这样的实体中,它们很难测试、难以阅读、难以维护,但我不喜欢这样的真正原因方法是:如果验证规则发生变化,我必须来编辑我的域实体。这是一个非常简单的例子,但在 RL 中,验证可能更复杂
所以遵循 Udi Dahan 的哲学,明确角色,以及蓝皮书中 Eric Evans 的建议,接下来的尝试是实现规范模式,像这样
class EmailDomainIsAllowedSpecification : IDomainSpecification<Customer>
{
private INotAllowedEmailDomainsResolver invalidEmailDomainsResolver;
public bool IsSatisfiedBy(Customer customer)
{
return !this.invalidEmailDomainsResolver.GetInvalidEmailDomains().Contains(customer.Email);
}
}
但后来我意识到,为了遵循这种方法,我必须首先改变我的实体以传递被验证的值,在这种情况下是电子邮件,但改变它们会导致我的域事件被触发,我不想这样做在新电子邮件有效之前发生
所以在考虑了这些方法之后,我提出了这个,因为我要实现一个 CQRS 架构:
class EmailDomainIsAllowedValidator : IDomainInvariantValidator<Customer, ChangeEmailCommand>
{
public void IsValid(Customer entity, ChangeEmailCommand command)
{
if(!command.Email.HasValidDomain()) throw new DomainException(“...”);
}
}
这就是主要思想,实体被传递给验证器,以防我们需要来自实体的一些值来执行验证,命令包含来自用户的数据,并且由于验证器被认为是可注入对象,它们可以注入外部依赖项如果验证需要它。
现在的困境,我对这样的设计很满意,因为我的验证被封装在单个对象中,这带来了许多优点:易于单元测试,易于维护,域不变量使用通用语言显式表达,易于扩展,验证逻辑是集中式和验证器可以一起使用来执行复杂的域规则。即使我知道我正在将我的实体的验证放在它们之外(你可能会争论代码味道 - 贫血域),但我认为权衡是可以接受的
但是有一件事我还没有弄清楚如何以一种干净的方式实现它。我应该如何使用这些组件...
由于它们将被注入,它们不会自然地适合我的域实体,所以基本上我看到了两个选项:
将验证器传递给我的实体的每个方法
在外部验证我的对象(从命令处理程序)
我对选项 1 不满意,所以我将解释如何使用选项 2
class ChangeEmailCommandHandler : ICommandHandler<ChangeEmailCommand>
{
// here I would get the validators required for this command injected
private IEnumerable<IDomainInvariantValidator> validators;
public void Execute(ChangeEmailCommand command)
{
using (var t = this.unitOfWork.BeginTransaction())
{
var customer = this.unitOfWork.Get<Customer>(command.CustomerId);
// here I would validate them, something like this
this.validators.ForEach(x =. x.IsValid(customer, command));
// here I know the command is valid
// the call to ChangeEmail will fire domain events as needed
customer.ChangeEmail(command.Email);
t.Commit();
}
}
}
好吧,就是这样。你能告诉我你对此的看法或分享你在域实体验证方面的经验吗
编辑
我认为我的问题并不清楚,但真正的问题是:隐藏域规则对应用程序的未来可维护性有严重影响,并且域规则在应用程序的生命周期中经常发生变化。因此,考虑到这一点来实现它们将使我们能够轻松地扩展它们。现在想象将来实现一个规则引擎,如果规则被封装在领域实体之外,这种改变会更容易实现
我知道将验证放在我的实体之外会破坏封装,正如@jgauffin 在他的回答中提到的那样,但我认为将验证放在单个对象中的好处比仅仅保持实体的封装要大得多。现在我认为封装在传统的 n 层架构中更有意义,因为实体在域层的多个地方使用,但在 CQRS 架构中,当命令到达时,将有一个命令处理程序访问聚合根和对聚合根执行操作只会创建一个完美的窗口来放置验证。
我想对将验证放置在实体中与将其放置在单个对象中的优点进行一个小的比较
单个对象中的验证
- 临。容易写
- 临。易于测试
- 临。明确表达了
- 临。它成为领域设计的一部分,用当前的通用语言表达
- 临。由于它现在是设计的一部分,因此可以使用 UML 图对其进行建模
- 临。极易维护
- 临。使我的实体和验证逻辑松散耦合
- 临。易于扩展
- 临。遵循 SRP
- 临。遵循打开/关闭原则
- 临。不违反得墨忒耳定律(嗯)?
- 临。我是集中的
- 临。它可以重复使用
- 临。如果需要,可以轻松注入外部依赖项
- 临。如果使用插件模型,只需删除新程序集即可添加新验证器,而无需重新编译整个应用程序
- 临。实现规则引擎会更容易
- 骗局。打破封装
- 骗局。如果封装是强制性的,我们必须将单个验证器传递给实体(聚合)方法
封装在实体内部的验证
- 临。封装?
- 临。可重复使用的?
我很想看看你对此的看法