您如何处理域驱动设计中复杂聚合的验证?您是否正在整合您的业务规则/验证逻辑?
我了解参数验证,并且了解可以附加到模型本身的属性验证,并执行诸如检查电子邮件地址或邮政编码是否有效或名字具有最小和最大长度之类的操作。
但是涉及多个模型的复杂验证呢?您通常将这些规则和方法放在您的架构中的什么位置?如果有的话,你用什么模式来实现它们?
您如何处理域驱动设计中复杂聚合的验证?您是否正在整合您的业务规则/验证逻辑?
我了解参数验证,并且了解可以附加到模型本身的属性验证,并执行诸如检查电子邮件地址或邮政编码是否有效或名字具有最小和最大长度之类的操作。
但是涉及多个模型的复杂验证呢?您通常将这些规则和方法放在您的架构中的什么位置?如果有的话,你用什么模式来实现它们?
与其依赖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
我喜欢 Jimmy Bogard 对这个问题的解决方案。他在他的博客上发表了一篇题为“使用访问者和扩展方法进行实体验证”的文章,其中他提出了一种非常优雅的实体验证方法,建议实现一个单独的类来存储验证代码。
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;
}
}
我通常使用规范类,它提供了一个方法(这是 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
}
}
我喜欢这种方法,因为它允许我将所有核心验证逻辑放在一个地方,从而使事情变得简单。