C# 将服务层与验证层分离

C# 将服务层与验证层分离,c#,asp.net-mvc-3,service-layer,C#,Asp.net Mvc 3,Service Layer,我目前有一个基于ASP.NET站点文章的服务层 根据答案,这是一种糟糕的方法,因为服务逻辑与验证逻辑混合在一起,这违反了单一责任原则 我真的很喜欢提供的替代方案,但在重新分解代码的过程中,我遇到了一个无法解决的问题 考虑以下服务接口: interface IPurchaseOrderService { void CreatePurchaseOrder(string partNumber, string supplierName); } interface IPurchaseOrderS

我目前有一个基于ASP.NET站点文章的服务层

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

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

考虑以下服务接口:

interface IPurchaseOrderService
{
    void CreatePurchaseOrder(string partNumber, string supplierName);
}
interface IPurchaseOrderService
{
    void CreatePurchaseOrder(CreatePurchaseOrder command);
}
根据链接答案,具体实施如下:

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
对象还需要另外两个实体,
Part
Supplier
(在本例中,假设采购订单只有一个零件)

如果用户提供的详细信息与数据库中需要验证器抛出异常的实体不一致,则
零件
供应商
对象都可能为空

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

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


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

简短回答:

你在验证错误的东西

很长的回答:

您正在尝试验证
采购订单
,但这是一个实现细节。相反,您应该验证的是操作本身,在本例中是
partNumber
supplierName
参数

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

长话短说,问题在于您的
IPurchaseOrderService
接口。它不应该接受两个字符串参数,而是一个单独的参数(a)。让我们调用这个参数对象
CreatePurchaseOrder

使用更改后的
IPurchaseOrderService
界面:

interface IPurchaseOrderService
{
    void CreatePurchaseOrder(string partNumber, string supplierName);
}
interface IPurchaseOrderService
{
    void CreatePurchaseOrder(CreatePurchaseOrder command);
}
CreatePurchaseOrder
参数对象包装原始参数。此参数对象是描述创建采购订单意图的消息。换句话说,这是一个命令

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

但是为什么要由
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);
    }
}
当然,这种方法的问题是,为系统中的每个服务定义这样的装饰器类会非常尴尬。这将导致严重的代码发布

但问题是由一个缺陷引起的。为特定服务定义接口(例如
IPurchaseOrderService
)通常是有问题的。您定义了
CreatePurchaseOrder
,因此已经有了这样的定义。现在,您可以为系统中的所有业务操作定义一个抽象:

public interface ICommandHandler<TCommand>
{
    void Handle(TCommand command);
}
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);
    }
}
使用此设计,您现在可以定义一个通用装饰器来处理系统中每个业务操作的所有验证:

public interface ICommandHandler<TCommand>
{
    void Handle(TCommand command);
}
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);
    }
}
但是由于这个装饰器是通用的,所以可以将它包装到系统中的每个命令处理程序中。哇!干得怎么样

这种设计也使得以后添加横切关注点变得非常容易。例如,您的服务当前似乎负责对工作单元调用
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 CreatePurchaseOrder
{
    public int PartId;
    public int SupplierId;
}
当您执行此操作时,您不必检查具有给定名称的零件是否存在。表示层(或外部系统)向您传递了一个ID,因此您不再需要验证该部件的存在。当然,如果该ID没有任何部分,那么命令处理程序应该失败,但在这种情况下,可能会出现编程错误或并发冲突。无论哪种情况,都不需要将用户友好的验证错误反馈给客户机

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

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

  • 实体序列化的问题消失了,因为命令可以很容易地序列化和模型绑定
  • DataAnnotation属性可以很容易地应用于命令,这支持客户端(Javascript)验证
  • decorator可以应用于在数据库事务中封装完整操作的所有命令处理程序
  • 它删除了循环引用betwe