将服务层与验证层分离

Posted

技术标签:

【中文标题】将服务层与验证层分离【英文标题】:Separating the service layer from the validation layer 【发布时间】:2013-05-23 12:55:16 【问题描述】:

我目前有一个基于 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();

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

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

【问题讨论】:

【参考方案1】:

简答:

你在验证错误的东西。

很长的答案:

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

单独验证这两个参数会很尴尬,但这是由您的设计造成的——您缺少抽象。

长话短说,问题在于您的IPurchaseOrderService 界面。它不应该采用两个字符串参数,而应该采用一个单一参数(Parameter Object)。我们称之为参数对象CreatePurchaseOrder

public class CreatePurchaseOrder

    public string PartNumber;
    public string SupplierName;

使用更改后的IPurchaseOrderService 接口:

interface IPurchaseOrderService

    void CreatePurchaseOrder(CreatePurchaseOrder command);

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

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

但是为什么IPurchaseOrderService 负责验证呢? 验证是一个横切关注点,您应该避免将其与业务逻辑混为一谈。相反,您可以为此定义一个装饰器:

public class ValidationPurchaseOrderServiceDecorator : IPurchaseOrderService

    private readonly IValidator<CreatePurchaseOrder> validator;
    private readonly IPurchaseOrderService decoratee;

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

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

这样你就可以通过简单地包装一个真正的PurchaseOrderService来添加验证:

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

当然,使用这种方法的问题是,为系统中的每个服务定义这样的装饰器类真的很尴尬。这将导致严重的代码发布。

但问题是由缺陷引起的。为每个特定服务定义一个接口(例如IPurchaseOrderService)通常是有问题的。您定义了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 IValidator<T> validator;
    private readonly ICommandHandler<T> decoratee;

    ValidationCommandHandlerDecorator(
        IValidator<T> validator, ICommandHandler<T> decoratee)
    
        this.validator = validator;
        this.decoratee = decoratee;
    

    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 CreatePurchaseOrderValidator(),
        new CreatePurchaseOrderHandler());

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

这种设计还使得以后添加横切关注点变得非常容易。例如,您的服务目前似乎负责在工作单元上调用 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.GetByNumber(command.PartNumber);

        if (part == null)
        
            yield return new ValidationResult("Part Number", 
                $"Part number command.PartNumber does not exist.");
        

        var supplier = this.supplierRepository.GetByName(command.SupplierName);

        if (supplier == null)
        
            yield return new ValidationResult("Supplier Name", 
                $"Supplier named command.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。但我仍然体验到了这一点,使系统更容易和可扩展。

它还解决了您所指文章的 cmets 部分所述的大部分问题,例如:

实体序列化的问题消失了,因为命令可以轻松序列化和模型绑定。 DataAnnotation 属性可以很容易地应用于命令,这可以实现客户端 (javascript) 验证。 装饰器可应用于将完整操作包装在数据库事务中的所有命令处理程序。 它消除了控制器和服务层之间的循环引用(通过控制器的 ModelState),从而使控制器不再需要新的服务类。

如果您想了解有关此类设计的更多信息,您绝对应该查看this article。

【讨论】:

+1 谢谢,非常感谢。我将不得不离开并评估信息,因为有很多东西要消化。顺便说一句,我目前正在考虑从 Ninject 转移到 Simple Injector。我读过关于性能的好东西,但卖给我的是简单 Injector 的文档要好得多。 您能否详细说明传递给装饰器的PurchaseOrderCommandHandlerPurchaseOrderCommandValidator 之间的区别,因为它们似乎做同样的事情?验证器是否打算将实体实例作为参数而不是命令对象? PurchaseOrderCommandValidator 检查PurchaseOrderCommandHandler 执行的先决条件。如果需要,它将通过检查零件和供应商是否存在来查询数据库以了解处理程序是否可以正确执行。 抱歉,我还是有点不确定。你能举一个PurchaseOrderCommandValidator的例子吗?我在想这是相反的,即PurchaseOrderCommandHandler 检查前置条件(来自数据库)并将PurchaseOrder 对象传递给PurchaseOrderValidator(而不是PurchaseOrderCommand 对象) 这让事情变得更加清晰。再次感谢您的帮助。

以上是关于将服务层与验证层分离的主要内容,如果未能解决你的问题,请参考以下文章

将数据访问层与服务层分开是否很好[关闭]

Jalo 层与服务层

表示层与业务层分离

C#(VS2019):当两者都使用使用 VS 项目结构的类时,如何将域层与数据层分离

业务层与服务层的服务引用

属性 每秒10万吞吐 并发 架构 设计 58最核心的帖子中心服务IMC 类目服务 入口层是Java研发的,聚合层与检索层都是C语言研发的 电商系统里的SKU扩展服务