2013-05-28 131 views
25

我目前有一个基于ASP.NET网站上文章Validating with a service layer的服务层。将服务层与验证层分开

根据this回答,这是一种糟糕的方法,因为服务逻辑与验证逻辑混合在一起,违反了单一责任原则。

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

考虑以下服务接口:

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(); 
    } 
} 

传递给验证还需要其他两个实体的PurchaseOrder对象, PartSupplier(让我们假设这个例子PO只有一个部分)。

如果用户提供的详细信息与数据库中不需要验证器引发异常的实体不匹配,则PartSupplier对象可能为空。

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

从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(); 
} 

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

有没有一种获得两全其美的方法?我是否可以将服务层与验证层分开,同时仍提供相同级别的错误信息?

回答

42

简短的回答:

您要验证错误的事情。

很长的回答:

您试图验证PurchaseOrder但是这是一个实现细节。相反,你应该验证的是操作本身,在这种情况下,参数为partNumbersupplierName

验证这两个参数本身会很尴尬,但这是由您的设计引起的 - 您错过了一个抽象。

长话短说,问题在于你的​​界面。它不应该接受两个字符串参数,而是一个参数(一个Parameter Object)。我们称之为参数对象:CreatePurchaseOrder。在这种情况下,该接口是这样的:

public class CreatePurchaseOrder 
{ 
    public string PartNumber; 
    public string SupplierName; 
} 

interface IPurchaseOrderService 
{ 
    void CreatePurchaseOrder(CreatePurchaseOrder command); 
} 

参数对象CreatePurchaseOrder包装原有参数。该参数对象是描述创建采购订单意向的消息。换句话说:这是一个命令

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

但是为什么​​负责验证? 验证是一个交叉问题,您应该尝试阻止将其与业务逻辑混合。相反,你可以定义一个装饰了这一点:

public class ValidationPurchaseOrderServiceDecorator : IPurchaseOrderService 
{ 
    private readonly IPurchaseOrderService decoratee; 
    private readonly IValidator<CreatePurchaseOrder> validator; 

    ValidationPurchaseOrderServiceDecorator(IPurchaseOrderService decoratee, 
     IValidator<CreatePurchaseOrder> validator) 
    { 
     this.decoratee = decoratee; 
     this.validator = validator; 
    } 

    public void CreatePurchaseOrder(CreatePurchaseOrder command) 
    { 
     this.validator.Validate(command); 
     this.decoratee.CreatePurchaseOrder(command); 
    } 
} 

这样我们就可以通过简单地包裹一个真正PurchaseOrderService添加验证:当然

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

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

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

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

有了这个抽象,我们现在可以重构PurchaseOrderService以下几点:

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

     unitOfWork.Savechanges(); 
    } 
} 

采用这种设计,我们现在可以定义一个单一通用的装饰处理验证系统中的每一个业务操作:

public class ValidationCommandHandlerDecorator<T> : ICommandHandler<T> 
{ 
    private readonly ICommandHandler<T> decoratee; 
    private readonly IValidator<T> validator; 

    ValidationCommandHandlerDecorator(
     ICommandHandler<T> decoratee, IValidator<T> 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,但现在作为通用类。这个装饰可以围绕我们的新服务类包裹:

var service = 
    new ValidationCommandHandlerDecorator<PurchaseOrderCommand>(
     new CreatePurchaseOrderHandler(), 
     new CreatePurchaseOrderValidator()); 

但由于这个装饰是通用的,我们可以在我们的系统中它环绕的每一个命令处理程序。哇!那是干什么的?

此设计也使得以后很容易增加横切关注点。例如,您的服务目前似乎负责在工作单元上调用SaveChanges。这也可以被认为是一个交叉问题,可以很容易地提取给装饰者。通过这种方式,您的服务类变得更简单,只需较少的代码即可进行测试。

CreatePurchaseOrder验证可以看看如下:

public sealed class CreatePurchaseOrderValidator : IValidator<CreatePurchaseOrder> 
{ 
    private readonly IRepository<Part> partsRepository; 
    private readonly IRepository<Supplier> supplierRepository; 

    public CreatePurchaseOrderValidator(IRepository<Part> partsRepository, 
     IRepository<Supplier> supplierRepository) 
    { 
     this.partsRepository = partsRepository; 
     this.supplierRepository = supplierRepository; 
    } 

    protected override IEnumerable<ValidationResult> 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<CreatePurchaseOrder> 
{ 
    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); 
    } 
} 

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

请注意,如果您的命令包含尽可能多的ID,它可能会让您的生活更轻松。当你这样做,你就不必检查如果给定名称的一部分确实存在

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

:所以,你的系统将可以受益于定义命令,如下所示。表示层(或外部系统)向您传递了一个Id,因此您不必再验证该部分的存在。当没有该ID的一部分时,命令处理程序当然会失败,但在这种情况下,会出现编程错误或并发冲突。在任何情况下,都不需要将表达性的用户友好的验证错误传达给客户。

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

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

  • 由于命令都可以轻松地序列和模型结合,与实体序列化问题消失了。
  • DataAnnotation属性可以很容易地应用于命令,并且这可以实现客户端(Javascript)验证。
  • 装饰器可以应用于所有命令处理程序,它将数据库事务中的完整操作封装起来。
  • 它删除控制器和服务层之间的循环引用(通过控制器的ModelState),不再需要控制器来创建新的服务类。

如果你想了解更多关于这种类型的设计,你应该绝对检查出this article

+1

+1谢谢,非常感谢。由于需要消化很多东西,我将不得不离开并评估信息。顺便说一下,我目前正在从Ninject转移到Simple Injector。我已经读过关于性能的好消息,但把它卖给我的是,简单喷油器的文档要好得多。 –

+0

能否详细说明传递给装饰器的'PurchaseOrderCommandHandler'和'PurchaseOrderCommandValidator'之间的差异,因为它们似乎做同样的事情?验证者的意图是将实体的实例作为参数而不是命令对象吗? –

+0

'PurchaseOrderCommandValidator'检查执行'PurchaseOrderCommandHandler'的先决条件。如果需要,它将通过检查零件和供应商是否存在来查询数据库,以确定处理程序是否可以正确执行。 – Steven