从验证层分离服务层 [英] Separating the service layer from the validation layer

查看:127
本文介绍了从验证层分离服务层的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我现在有根据的文章<一个服务层href=\"http://www.asp.net/mvc/tutorials/older-versions/models-%28data%29/validating-with-a-service-layer-cs\">Validating从ASP.NET站点服务层。

根据这个答案,这是个不错的办法,因为业务逻辑与它违反了验证逻辑混单一职责原则。

According to this answer, this is a bad approach because the service logic is mixed with the validation logic which violates the single responsibility principle.

我很喜欢那个提供,但我的code的再保理我接触过,我无法来解决问题过程中的选择。

I really like the alternative that is supplied but during re-factoring of my code I have come across a problem that I am unable to solve.

考虑下面的服务接口:

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

与基于链接的回答以下具体落实:

with the following concrete implementation based on the linked answer:

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 对象还需要其他两个实体,部分供应商(假设在这个例子中,一个PO只有一个组成部分)。

The PurchaseOrder object that is passed to the validator also requires two other entities, Part and Supplier (let's assume for this example that a PO only has a single part).

无论是部分公司对象可以为空,如果由用户提供的信息不符合实体在数据库,将要求验证抛出异常。

Both the Part and Supplier objects could be null if the details supplied by the user do not correspond to entities in the database which would require the validator to throw an exception.

我的问题是,在此阶段的验证已经失去了上下文信息(部件编号和供应商名称),以便不能报告准确的错误给用户。我能提供的最好的错误是沿行的采购订单必须有关联的部分的这不会有意义给用户,因为他们没有提供一个部件号(它只是不存在数据库)。

The problem I have is that at this stage the validator has lost the contextual information (the part number and the supplier name) so is unable to report an accurate error to the user. The best error I can supply is along the lines of "A purchase order must have an associated part" which would not make sense to the user because they did supply a part number (it just does not exist in the database).

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

Using the service class from the ASP.NET article I am doing something like this:

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

这使我能够提供更好的验证信息给用户,但表示该验证逻辑在服务类中直接包含,违反了单一职责原则(code也复制服务类之间)。

This allows me to provide much better validation information to the user but means that the validation logic is contained directly in the service class, violating the single responsibility principle (code is also duplicated between service classes).

有没有得到两全其美的方法吗?可我同时还提供错误信息的同一级别的服务层从验证层分开?

Is there a way of getting the best of both worlds? Can I separate the service layer from the validation layer whilst still providing the same level of error information?

推荐答案

简短的回答:

您的验证错误的事情。

很长的回答:

您正试图验证一个的PurchaseOrder 但是这是一个实现细节。相反,你应该确认的是操作本身,在这种情况下,部分号码 supplierName 参数。

You are trying to validate an PurchaseOrder but that is an implementation detail. Instead what you should validate is the operation itself, in this case the partNumber and supplierName parameters.

自己验证这两个参数会很尴尬,但是这是由设计造成的。你错过了一个抽象。

Validating those two parameters by themselves would be awkward, but this is caused by your design. You're missing an abstraction.

长话短说,这个问题是在你的 IPurchaseOrderService 接口。它不应该采取两个字符串参数,而是一个参数(一个参数对象)。让我们把这个参数对象: PurchaseOrderCommand 。在这种情况下,该接口是这样的:

Long story short, the problem is in your IPurchaseOrderService interface. It shouldn't take two string arguments, but one argument (a Parameter Object). Let's call this parameter object: PurchaseOrderCommand. In that case the interface would look like this:

public class PurchaseOrderCommand
{
    public string PartNumber { get; set; }
    public string SupplierName { get; set; }
}

interface IPurchaseOrderService
{
    void CreatePurchaseOrder(PurchaseOrderCommand command);
}

现在,你可以创建一个 IValidator&LT; Pur​​chaseOrderCommand方式&gt; 的实施,可以做所有正确的验证包括检查正确的零部件供应商的存在,并报告用户友好的错误信息

Now you can create an IValidator<PurchaseOrderCommand> implementation that can do all the proper validations including checking the existence of the proper parts supplier and reporting user friendly error messages.

但为什么在 IPurchaseOrderService 负责验证?验证是一个横切关注点,你应该尝试prevent业务逻辑混合。相反,你应该定义一个装饰这个:

But why is the IPurchaseOrderService be responsible for the validation? Validation is a cross-cutting concern and you should try to prevent mixing it with business logic. Instead you should define a decorator for this:

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

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

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

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

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

当然,这种方法的问题是,这将是非常尴尬的定义这种装饰类为系统中的每个服务。这将是一个严重违反了DRY原则。

Problem of course with this approach is that it would be really awkward to define such decorator class for each service in the system. That would be a severe violation of the DRY principle.

但问题是通过在设计的一个缺陷引起的。定义每个特定的服务接口(如 IPurchaseOrderService )是有问题的。由于我们所定义的 PurchaseOrderCommand 我们已经拥有了这样的定义。取而代之的是,一个单一的抽象系统中的所有业务操作:

But the problem is caused by a flaw in the design. Defining an interface per specific service (such as the IPurchaseOrderService) is problematic. Since we defined the PurchaseOrderCommand we already have such a definition. Instead, define one single abstraction for all business operations in the system:

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

通过这个抽象,我们现在可以定义新的PurchaseOrderService如下:

With this abstraction we can now define the new PurchaseOrderService as follows:

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

        unitOfWork.Savechanges();
    }
}

通过这样的设计,我们现在可以定义一个通用的单一装饰处理验证系统中的每一个业务方面:

With this design, we can now define one single generic decorator to handle validations for every business operation in the system:

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

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

    void Handle(T command)
    {
        this.validator.Validate(command);
        this.decoratee.Handle(command);
    }
}

注意这个装饰是如何几乎相同pviously定义 ValidationPurchaseOrderServiceDecorator 的$ P $,但现在必须做通用的。这个装饰可以围绕我们的新服务类包裹:

Notice how this decorator is almost the same as the previously defined ValidationPurchaseOrderServiceDecorator, but now must made generic. This decorator can be wrapped around our new service class:

var service =
    new ValidationCommandHandlerDecorator<PurchaseOrderCommand>(
        new PurchaseOrderCommandHandler(),
        new PurchaseOrderCommandValidator());

但由于这个装饰是通用的,我们可以在我们的系统将其套在每一个命令处理程序。

But since this decorator is generic, we can wrap it around every command handler in our system.

这样的设计使得它可以很方便以后添加横切关注点。例如,你的服务目前看来负责调用的SaveChanges 工作单位。这可以被认为是一个横切关注点,以及和可以很容易地提取到一个装饰。这样,你的服务类成为少code简单所剩无几测试。

This design makes it really easy to add cross-cutting concerns later on. For instance, your service currently seems responsible for calling SaveChanges on the unit of work. This can be considered a cross-cutting concern as well and can easily be extracted to a decorator. This way your service classes become much simpler with less code left to test.

您的验证应该是这样的:

Your validator would look like this:

public sealed class PurchaseOrderCommandValidator : Validator<PurchaseOrderCommand>
{
    private IRepository<Part> partsRepository;
    private IRepository<Supplier> supplierRepository;

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

    protected override IEnumerable<ValidationResult> Validate(
        PurchaseOrderCommand command)
    {
        var part = this.partsRepository.Get(p => p.Number == command.PartNumber);

        if (part == null)
        {
            yield return new ValidationResult("Part Number", 
                string.Format("Part number {0} does not exist.", 
                    partNumber));
        }

        var supplier = this.supplierRepository.Get(p => p.Name == command.SupplierName);

        if (supplier == null)
        {
            yield return new ValidationResult("Supplier Name", 
                string.Format("Supplier named {0} does not exist.", 
                    supplierName));
        }
    }
}

和您的命令处理程序是这样的:

And your command handler like this:

public class PurchaseOrderCommandHandler : ICommandHandler<PurchaseOrderCommand>
{
    private IUnitOfWork uow;

    public PurchaseOrderCommandHandler(IUnitOfWork uow)
    {
        this.uow = uow;
    }

    public void Handle(PurchaseOrderCommand 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);
    }
}

请注意,命令将成为你的域的一部分。有用例和命令和代替验证实体,这些实体将是一个实施的细节之间的一对一的映射。这些命令成为了合同,将得到验证。

Note that commands will become part of your domain. There is a one-to-one mapping between use cases and commands and instead of validating entities, those entities will be an implementation detail. The commands become the contract and will get validation.

请注意,你可能让你的生活更容易,如果你的命令包含尽可能多的ID越好。所以,你的系统将能够从定义命令受益如下:

Note that you will probably make your life much easier if your commands contain as much IDs as possible. So your system would could benefit from defining a command as follows:

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

当你这样做,你就不必检查如果给定名称的一部分确实存在。在presentation层(或外部系统),通过您的ID,所以你不必验证该部分的存在了。当有通过ID号的一部分,但在这​​种情况下,有两种编程错误或并发冲突命令处理器当然应该失败。在这两种情况下没有必要进行通信前pressive用户友好验证错误返回给客户端。

When you do this you won't have to check if a part by the given name does exist. The presentation layer (or an external system) passed you an Id, so you don't have to validate the existence of that part anymore. The command handler should of course fail when there's no part by that ID, but in that case there is either a programming error or a concurrency conflict. In either case no need to communicate expressive user friendly validation errors back to the client.

然而,这确实把问题移到到presentation层。在presentation层,用户将必须从列表中选择一个部分让我们获得那部分的ID。但我仍经历了这使系统更容易和可扩展性。

This does however moves the problem to the presentation layer. In the presentation layer, the user will have to select a part from a list for us to get the ID of that part. But still I experienced the this to make the system much easier and scalable.

这也解决了大部分,在你指的是文章的注释部分列示的问题,如:

It also solves most of the problems that are stated in the comments section of the article you are referring to, such as:


  • 由于命令可以很容易地序列化和模型结合,与实体序列化问题消失。

  • DataAnnotation属性已经可以轻松应用到的命令,这使得客户端(JavaScript)的验证。

  • 装饰器可以应用于所有的命令处理程序包装在一个数据库事务的完成操作。

  • 它消除了控制器和服务层(通过控制器的ModelState中)之间的循环引用,取消对控制器的新服务类的需求。

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

If you want to learn more about this type of design, you should absolutely check out this article.

这篇关于从验证层分离服务层的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆