【发布时间】:2010-10-05 17:19:04
【问题描述】:
您如何处理域驱动设计中复杂聚合的验证?您是否正在整合您的业务规则/验证逻辑?
我了解参数验证,我了解可以附加到模型本身的属性验证,并执行诸如检查电子邮件地址或邮政编码是否有效或名字具有最小和最大长度之类的事情。
但是涉及多个模型的复杂验证呢?您通常将这些规则和方法放在您的架构中的什么位置?如果有的话,你用什么模式来实现它们?
【问题讨论】:
标签: validation domain-driven-design
您如何处理域驱动设计中复杂聚合的验证?您是否正在整合您的业务规则/验证逻辑?
我了解参数验证,我了解可以附加到模型本身的属性验证,并执行诸如检查电子邮件地址或邮政编码是否有效或名字具有最小和最大长度之类的事情。
但是涉及多个模型的复杂验证呢?您通常将这些规则和方法放在您的架构中的什么位置?如果有的话,你用什么模式来实现它们?
【问题讨论】:
标签: validation domain-driven-design
不要在整个应用程序中依赖 IsValid(xx) 调用,而是考虑从 Greg Young 那里获得一些建议:
永远不要让你的实体进入 无效状态。
这基本上意味着您不再将实体视为纯数据容器,而更多地考虑具有行为的对象。
考虑一个人的地址的例子:
person.Address = "123 my street";
person.City = "Houston";
person.State = "TX";
person.Zip = 12345;
在任何这些调用之间,您的实体都是无效的(因为您的属性彼此不一致。现在考虑一下:
person.ChangeAddress(.......);
所有与更改地址行为相关的调用现在都是一个原子单元。您的实体在这里永远是无效的。
如果您采用这种建模行为而不是状态的想法,那么您可以获得一个不允许无效实体的模型。
如需对此进行深入讨论,请查看此 infoq 采访:http://www.infoq.com/interviews/greg-young-ddd
【讨论】:
Things。在处理的下一个阶段,在它被填充之后,我必须确保列表具有一定的元素,并且它是它的唯一元素。我怎样才能让Things 不进入无效状态?作为一种可能的解决方案,我认为要求用户提供此对象作为列表的第一个元素,但我不能这样做。所以对象Things 可以处于它的转换状态,它可以一直保持到下一个阶段的处理开始。这就是为什么我必须在处理之前对其进行验证。
我喜欢 Jimmy Bogard 对这个问题的解决方案。他在他的博客上有一篇名为 "Entity validation with visitors and extension methods" 的帖子,其中他提出了一种非常优雅的实体验证方法,建议实现一个单独的类来存储验证代码。
public interface IValidator<T>
{
bool IsValid(T entity);
IEnumerable<string> BrokenRules(T entity);
}
public class OrderPersistenceValidator : IValidator<Order>
{
public bool IsValid(Order entity)
{
return BrokenRules(entity).Count() == 0;
}
public IEnumerable<string> BrokenRules(Order entity)
{
if (entity.Id < 0)
yield return "Id cannot be less than 0.";
if (string.IsNullOrEmpty(entity.Customer))
yield return "Must include a customer.";
yield break;
}
}
【讨论】:
return BrokenRules(entity).Count() > 0 不应该是 return BrokenRules(entity).Count() <= 0 吗?
我通常使用规范类, 它提供了一种方法(这是 C#,但您可以将其翻译成任何语言):
bool IsVerifiedBy(TEntity candidate)
此方法对候选人及其关系进行全面检查。 您可以在规范类中使用参数来使其参数化,例如检查级别...
您还可以添加一个方法来了解候选人未验证规范的原因:
IEnumerable<string> BrokenRules(TEntity canditate)
您可以简单地决定像这样实现第一个方法:
bool IsVerifiedBy(TEntity candidate)
{
return BrokenRules(candidate).IsEmpty();
}
对于违反规则,我通常会写一个迭代器:
IEnumerable<string> BrokenRules(TEntity candidate)
{
if (someComplexCondition)
yield return "Message describing cleary what is wrong...";
if (someOtherCondition)
yield return
string.Format("The amount should not be {0} when the state is {1}",
amount, state);
}
对于本地化,您应该使用资源,为什么不将文化传递给 BrokenRules 方法。 我将这些类放在模型命名空间中,其名称表明它们的用途。
【讨论】:
多个模型验证应该通过您的聚合根。如果您必须跨聚合根进行验证,则可能存在设计缺陷。
我对聚合进行验证的方式是返回一个响应接口,告诉我验证是否通过/失败以及有关失败原因的任何消息。
您可以验证聚合根上的所有子模型,以便它们保持一致。
// Command Response class to return from public methods that change your model
public interface ICommandResponse
{
CommandResult Result { get; }
IEnumerable<string> Messages { get; }
}
// The result options
public enum CommandResult
{
Success = 0,
Fail = 1
}
// My default implementation
public class CommandResponse : ICommandResponse
{
public CommandResponse(CommandResult result)
{
Result = result;
}
public CommandResponse(CommandResult result, params string[] messages) : this(result)
{
Messages = messages;
}
public CommandResponse(CommandResult result, IEnumerable<string> messages) : this(result)
{
Messages = messages;
}
public CommandResult Result { get; private set; }
public IEnumerable<string> Messages { get; private set; }
}
// usage
public class SomeAggregateRoot
{
public string SomeProperty { get; private set; }
public ICommandResponse ChangeSomeProperty(string newProperty)
{
if(newProperty == null)
{
return new CommandResponse(CommandResult.Fail, "Some property cannot be changed to null");
}
SomeProperty = newProperty;
return new CommandResponse(CommandResult.Success);
}
}
【讨论】:
这个问题现在有点老了,但如果有人感兴趣,这里是我如何在我的服务类中实现验证。
我在每个服务类中都有一个私有 Validate 方法,该方法接受一个实体实例和正在执行的操作,如果验证失败,则会引发自定义异常,其中包含违反规则的详细信息。
带有内置验证的示例 DocumentService
public class DocumentService : IDocumentService
{
private IRepository<Document> _documentRepository;
public DocumentService(IRepository<Document> documentRepository)
{
_documentRepository = documentRepository;
}
public void Create(Document document)
{
Validate(document, Action.Create);
document.CreatedDate = DateTime.Now;
_documentRepository.Create(document);
}
public void Update(Document document)
{
Validate(document, Action.Update);
_documentRepository.Update(document);
}
public void Delete(int id)
{
Validate(_documentRepository.GetById(id), Action.Delete);
_documentRepository.Delete(id);
}
public IList<Document> GetAll()
{
return _documentRepository
.GetAll()
.OrderByDescending(x => x.PublishDate)
.ToList();
}
public int GetAllCount()
{
return _documentRepository
.GetAll()
.Count();
}
public Document GetById(int id)
{
return _documentRepository.GetById(id);
}
// validation
private void Validate(Document document, Action action)
{
var brokenRules = new List<string>();
if (action == Action.Create || action == Action.Update)
{
if (string.IsNullOrWhiteSpace(document.Title))
brokenRules.Add("Title is required");
if (document.PublishDate == null)
brokenRules.Add("Publish Date is required");
}
if (brokenRules.Any())
throw new EntityException(string.Join("\r\n", brokenRules));
}
private enum Action
{
Create,
Update,
Delete
}
}
我喜欢这种方法,因为它让我可以将所有核心验证逻辑放在一个地方,让事情变得简单。
【讨论】: