将服务层与验证层分开

Ben*_*ale 32 c# service-layer asp.net-mvc-3

我目前有一个服务层,它基于ASP.NET站点中的服务层验证一文.

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

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

请考虑以下服务接口:

interface IPurchaseOrderService
{
    void CreatePurchaseOrder(string partNumber, string supplierName);
}
Run Code Online (Sandbox Code Playgroud)

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

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();
    }
}
Run Code Online (Sandbox Code Playgroud)

PurchaseOrder传递给验证器的对象还需要两个其他实体,Part并且Supplier(假设此示例中PO只有一个部分).

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

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

使用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();
}
Run Code Online (Sandbox Code Playgroud)

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

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

Ste*_*ven 55

简短回答:

你正在验证错误的东西.

答案很长:

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

自己验证这两个参数会很尴尬,但这是由你的设计引起的 - 你错过了一个抽象.

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

public class CreatePurchaseOrder
{
    public string PartNumber;
    public string SupplierName;
}
Run Code Online (Sandbox Code Playgroud)

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

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

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

interface IPurchaseOrderService
{
    void CreatePurchaseOrder(CreatePurchaseOrder command);
}
Run Code Online (Sandbox Code Playgroud)

这样我们就可以通过简单地包裹一个真正添加验证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);
    }
}
Run Code Online (Sandbox Code Playgroud)

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

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

var service =
    new ValidationPurchaseOrderServiceDecorator(
        new CreatePurchaseOrderValidator(),
        new PurchaseOrderService());
Run Code Online (Sandbox Code Playgroud)

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

public interface ICommandHandler<TCommand>
{
    void Handle(TCommand command);
}
Run Code Online (Sandbox Code Playgroud)

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

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

        unitOfWork.Savechanges();
    }
}
Run Code Online (Sandbox Code Playgroud)

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

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);
    }
}
Run Code Online (Sandbox Code Playgroud)

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

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

SaveChanges验证可以如下所示:

var service =
    new ValidationCommandHandlerDecorator<PurchaseOrderCommand>(
        new CreatePurchaseOrderValidator(),
        new CreatePurchaseOrderHandler());
Run Code Online (Sandbox Code Playgroud)

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

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.");
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

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

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

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);
    }
}
Run Code Online (Sandbox Code Playgroud)

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

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

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

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

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