将服务层与validation层分开

我目前有一个服务层,它基于ASP.NET站点中的“ 使用服务层validation”一文 。

根据这个答案,这是一个糟糕的方法,因为服务逻辑与违反单一责任原则的validation逻辑混合在一起。

我真的很喜欢提供的替代方案,但在重新分解我的代码时,我遇到了一个我无法解决的问题。

请考虑以下服务接口:

interface IPurchaseOrderService { void CreatePurchaseOrder(string partNumber, string supplierName); } 

基于链接的答案具有以下具体实现:

 public class PurchaseOrderService : IPurchaseOrderService { public void CreatePurchaseOrder(string partNumber, string supplierName) { var po = new PurchaseOrder { Part = PartsRepository.FirstOrDefault(p => p.Number == partNumber), Supplier = SupplierRepository.FirstOrDefault(p => p.Name == supplierName), // Other properties omitted for brevity... }; validationProvider.Validate(po); purchaseOrderRepository.Add(po); unitOfWork.Savechanges(); } } 

传递给validation器的PurchaseOrder对象还需要另外两个实体, PartSupplier (我们假设PO只有一个部分)。

如果用户提供的详细信息与数据库中需要validation程序抛出exception的实体不对应,则PartSupplier对象都可以为null。

我遇到的问题是,在此阶段validation器丢失了上下文信息(部件号和供应商名称),因此无法向用户报告准确的错误。 我可以提供的最好的错误是“采购订单必须有一个相关的部分” ,这对用户没有意义,因为他们确实提供了部件号(它只是在数据库中不存在)。

使用ASP.NET文章中的服务类,我正在做这样的事情:

 public void CreatePurchaseOrder(string partNumber, string supplierName) { var part = PartsRepository.FirstOrDefault(p => p.Number == partNumber); if (part == null) { validationDictionary.AddError("", string.Format("Part number {0} does not exist.", partNumber); } var supplier = SupplierRepository.FirstOrDefault(p => p.Name == supplierName); if (supplier == null) { validationDictionary.AddError("", string.Format("Supplier named {0} does not exist.", supplierName); } var po = new PurchaseOrder { Part = part, Supplier = supplier, }; purchaseOrderRepository.Add(po); unitOfWork.Savechanges(); } 

这允许我向用户提供更好的validation信息,但意味着validation逻辑直接包含在服务类中,违反了单一责任原则(代码也在服务类之间重复)。

有没有办法让两全其美? 我是否可以将服务层与validation层分开,同时仍提供相同级别的错误信息?

简短回答:

你正在validation错误的东西。

答案很长:

您正在尝试validationPurchaseOrder但这是一个实现细节。 相反,您应该validation的是操作本身,在本例中是partNumbersupplierName参数。

自己validation这两个参数会很尴尬,但这是由你的设计引起的 – 你错过了一个抽象。

长话短说,问题出在你的IPurchaseOrderService接口上。 它不应该采用两个字符串参数,而是一个参数(一个参数对象 )。 让我们调用这个参数对象: CreatePurchaseOrder 。 在这种情况下,界面将如下所示:

 public class CreatePurchaseOrder { public string PartNumber; public string SupplierName; } interface IPurchaseOrderService { void CreatePurchaseOrder(CreatePurchaseOrder command); } 

参数对象CreatePurchaseOrder包装原始参数。 此参数对象是描述创建采购订单的打算的消息。 换句话说: 这是一个命令

使用此命令,您可以创建一个IValidator实现,该实现可以执行所有正确的validation,包括检查正确的零件供应商是否存在以及报告用户友好的错误消息。

但为什么IPurchaseOrderService负责validation? validation是一个跨领域的问题 ,您应该尝试防止将其与业务逻辑混合。 相反,你可以为此定义一个装饰器:

 public class ValidationPurchaseOrderServiceDecorator : IPurchaseOrderService { private readonly IPurchaseOrderService decoratee; private readonly IValidator validator; ValidationPurchaseOrderServiceDecorator(IPurchaseOrderService decoratee, IValidator validator) { this.decoratee = decoratee; this.validator = validator; } public void CreatePurchaseOrder(CreatePurchaseOrder command) { this.validator.Validate(command); this.decoratee.CreatePurchaseOrder(command); } } 

这样我们就可以通过简单地包装一个真正的PurchaseOrderService来添加validation:

 var service = new ValidationPurchaseOrderServiceDecorator( new PurchaseOrderService(), new CreatePurchaseOrderValidator()); 

当然,使用这种方法的问题是,为系统中的每个服务定义这样的装饰器类真的很尴尬。 这将严重违反DRY原则。

但问题是由一个缺陷造成的。 定义每个特定服务(例如IPurchaseOrderService )的接口通常是有问题的。 由于我们定义了CreatePurchaseOrder我们已经有了这样的定义。 我们现在可以为系统中的所有业务操作定义一个单一抽象:

 public interface ICommandHandler { void Handle(TCommand command); } 

通过这种抽象,我们现在可以将PurchaseOrderService重构为以下内容:

 public class CreatePurchaseOrderHandler : ICommandHandler { public void Handle(CreatePurchaseOrder command) { var po = new PurchaseOrder { Part = ..., Supplier = ..., }; unitOfWork.Savechanges(); } } 

通过这种设计,我们现在可以定义一个通用装饰器来处理系统中每个业务操作的validation:

 public class ValidationCommandHandlerDecorator : ICommandHandler { private readonly ICommandHandler decoratee; private readonly IValidator validator; ValidationCommandHandlerDecorator( ICommandHandler decoratee, IValidator validator) { this.decoratee = decoratee; this.validator = validator; } void Handle(T command) { var errors = this.validator.Validate(command).ToArray(); if (errors.Any()) { throw new ValidationException(errors); } this.decoratee.Handle(command); } } 

注意这个装饰器与先前定义的ValidationPurchaseOrderServiceDecorator几乎相同,但现在作为generics类。 这个装饰器可以包裹在我们的新服务类中:

 var service = new ValidationCommandHandlerDecorator( new CreatePurchaseOrderHandler(), new CreatePurchaseOrderValidator()); 

但由于这个装饰器是通用的,我们可以将它包装在我们系统中的每个命令处理程序中。 哇! 干嘛怎么样?

这种设计也使得以后添加横切关注点非常容易。 例如,您的服务当前似乎负责在工作单元上调用SaveChanges 。 这也可以被视为一个跨领域的问题,并且可以很容易地提取给装饰者。 这样,您的服务类变得更加简单,只需要更少的代码进行测试。

CreatePurchaseOrdervalidation器可能如下所示:

 public sealed class CreatePurchaseOrderValidator : IValidator { private readonly IRepository partsRepository; private readonly IRepository supplierRepository; public CreatePurchaseOrderValidator(IRepository partsRepository, IRepository supplierRepository) { this.partsRepository = partsRepository; this.supplierRepository = supplierRepository; } protected override IEnumerable Validate( CreatePurchaseOrder command) { var part = this.partsRepository.Get(p => p.Number == command.PartNumber); if (part == null) { yield return new ValidationResult("Part Number", $"Part number {partNumber} does not exist."); } var supplier = this.supplierRepository.Get(p => p.Name == command.SupplierName); if (supplier == null) { yield return new ValidationResult("Supplier Name", $"Supplier named {supplierName} does not exist."); } } } 

你的命令处理程序是这样的:

 public class CreatePurchaseOrderHandler : ICommandHandler { private readonly IUnitOfWork uow; public CreatePurchaseOrderHandler(IUnitOfWork uow) { this.uow = uow; } public void Handle(CreatePurchaseOrder command) { var order = new PurchaseOrder { Part = this.uow.Parts.Get(p => p.Number == partNumber), Supplier = this.uow.Suppliers.Get(p => p.Name == supplierName), // Other properties omitted for brevity... }; this.uow.PurchaseOrders.Add(order); } } 

请注意,命令消息将成为您域的一部分 。 用例和命令之间存在一对一的映射,而不是validation实体,这些实体将是一个实现细节。 这些命令成为合同并将得到validation。

请注意,如果您的命令包含尽可能多的ID,它可能会使您的生活更轻松。 因此,您的系统可以从定义命令中受益,如下所示:

 public class CreatePurchaseOrder { public int PartId; public int SupplierId; } 

执行此操作时,您不必检查给定名称的零件是否存在。 表示层(或外部系统)向您传递了一个Id,因此您不必再validation该部分的存在。 当该ID没有任何部分时,命令处理程序当然应该失败,但在这种情况下,存在编程错误或并发冲突。 在任何一种情况下,都不需要将富有表现力的用户友好validation错误传达回客户端。

然而,这确实将获得正确ID的问题转移到表示层。 在表示层中,用户必须从列表中选择一个部分,以便我们获取该部分的ID。 但我仍然体验到这一点,使系统更容易和可扩展。

它还解决了您所指的文章的评论部分中陈述的大多数问题,例如:

  • 由于命令可以很容易地序列化并且模型绑定,因此实体序列化的问题就消失了。
  • DataAnnotation属性可以轻松应用于命令,这使客户端(Javascript)validation成为可能。
  • 装饰器可以应用于包装数据库事务中的完整操作的所有命令处理程序。
  • 它删除了控制器和服务层之间的循环引用(通过控制器的ModelState),从而无需控制器来修改服务类。

如果您想了解有关此类设计的更多信息,请务必查看本文 。