草庐IT

c# - 将服务层与验证层分离

coder 2023-07-10 原文

我目前有一个基于文章 Validating with a service layer 的服务层来自 ASP.NET 站点。

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

我真的很喜欢提供的替代方案,但在重构我的代码期间,我遇到了一个我无法解决的问题。

考虑以下服务接口(interface):

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

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

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

最佳答案

简答:

你正在验证错误的事情。

很长的答案:

您正在尝试验证 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)定义一个接口(interface)通常是有问题的。您定义了 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);
    }
}

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

请注意,如果您的命令包含尽可能多的 ID,它可能会让您的生活更轻松。因此,您的系统可以从如下定义命令中受益:
public class CreatePurchaseOrder
{
    public int PartId;
    public int SupplierId;
}

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

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

它还解决了您所指文章的评论部分中所述的大部分问题,例如:
  • 实体序列化的问题消失了,因为命令可以轻松序列化和模型绑定(bind)。
  • DataAnnotation 属性可以轻松应用于命令,这可以启用客户端 (Javascript) 验证。
  • 装饰器可以应用于将完整操作包装在数据库事务中的所有命令处理程序。
  • 它消除了 Controller 和服务层之间的循环引用(通过 Controller 的 ModelState),消除了 Controller 新服务类的需要。

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

    关于c# - 将服务层与验证层分离,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/16793982/

    有关c# - 将服务层与验证层分离的更多相关文章

    1. ruby - 使用 ruby​​ 和 savon 的 SOAP 服务 - 2

      我正在尝试使用ruby​​和Savon来使用网络服务。测试服务为http://www.webservicex.net/WS/WSDetails.aspx?WSID=9&CATID=2require'rubygems'require'savon'client=Savon::Client.new"http://www.webservicex.net/stockquote.asmx?WSDL"client.get_quotedo|soap|soap.body={:symbol=>"AAPL"}end返回SOAP异常。检查soap信封,在我看来soap请求没有正确的命名空间。任何人都可以建议我

    2. ruby-on-rails - 如何验证 update_all 是否实际在 Rails 中更新 - 2

      给定这段代码defcreate@upgrades=User.update_all(["role=?","upgraded"],:id=>params[:upgrade])redirect_toadmin_upgrades_path,:notice=>"Successfullyupgradeduser."end我如何在该操作中实际验证它们是否已保存或未重定向到适当的页面和消息? 最佳答案 在Rails3中,update_all不返回任何有意义的信息,除了已更新的记录数(这可能取决于您的DBMS是否返回该信息)。http://ar.ru

    3. ruby - 具有身份验证的私有(private) Ruby Gem 服务器 - 2

      我想安装一个带有一些身份验证的私有(private)Rubygem服务器。我希望能够使用公共(public)Ubuntu服务器托管内部gem。我读到了http://docs.rubygems.org/read/chapter/18.但是那个没有身份验证-如我所见。然后我读到了https://github.com/cwninja/geminabox.但是当我使用基本身份验证(他们在他们的Wiki中有)时,它会提示从我的服务器获取源。所以。如何制作带有身份验证的私有(private)Rubygem服务器?这是不可能的吗?谢谢。编辑:Geminabox问题。我尝试“捆绑”以安装新的gem..

    4. ruby-on-rails - 如果为空或不验证数值,则使属性默认为 0 - 2

      我希望我的UserPrice模型的属性在它们为空或不验证数值时默认为0。这些属性是tax_rate、shipping_cost和price。classCreateUserPrices8,:scale=>2t.decimal:tax_rate,:precision=>8,:scale=>2t.decimal:shipping_cost,:precision=>8,:scale=>2endendend起初,我将所有3列的:default=>0放在表格中,但我不想要这样,因为它已经填充了字段,我想使用占位符。这是我的UserPrice模型:classUserPrice回答before_val

    5. ruby-on-rails - 如何验证非模型(甚至非对象)字段 - 2

      我有一个表单,其中有很多字段取自数组(而不是模型或对象)。我如何验证这些字段的存在?solve_problem_pathdo|f|%>... 最佳答案 创建一个简单的类来包装请求参数并使用ActiveModel::Validations。#definedsomewhere,atthesimplest:require'ostruct'classSolvetrue#youcouldevencheckthesolutionwithavalidatorvalidatedoerrors.add(:base,"WRONG!!!")unlesss

    6. ruby-on-rails - 启动 Rails 服务器时 ImageMagick 的警告 - 2

      最近,当我启动我的Rails服务器时,我收到了一长串警告。虽然它不影响我的应用程序,但我想知道如何解决这些警告。我的估计是imagemagick以某种方式被调用了两次?当我在警告前后检查我的git日志时。我想知道如何解决这个问题。-bcrypt-ruby(3.1.2)-better_errors(1.0.1)+bcrypt(3.1.7)+bcrypt-ruby(3.1.5)-bcrypt(>=3.1.3)+better_errors(1.1.0)bcrypt和imagemagick有关系吗?/Users/rbchris/.rbenv/versions/2.0.0-p247/lib/ru

    7. ruby-on-rails - s3_direct_upload 在生产服务器中不工作 - 2

      在Rails4.0.2中,我使用s3_direct_upload和aws-sdkgems直接为s3存储桶上传文件。在开发环境中它工作正常,但在生产环境中它会抛出如下错误,ActionView::Template::Error(noimplicitconversionofnilintoString)在View中,create_cv_url,:id=>"s3_uploader",:key=>"cv_uploads/{unique_id}/${filename}",:key_starts_with=>"cv_uploads/",:callback_param=>"cv[direct_uplo

    8. ruby-on-rails - 如何将验证与模型分开 - 2

      我有一些非常大的模型,我必须将它们迁移到最新版本的Rails。这些模型有相当多的验证(User有大约50个验证)。是否可以将所有这些验证移动到另一个文件中?说app/models/validations/user_validations.rb。如果可以,有人可以提供示例吗? 最佳答案 您可以为此使用关注点:#app/models/validations/user_validations.rbrequire'active_support/concern'moduleUserValidationsextendActiveSupport:

    9. ruby-on-rails - 跳过状态机方法的所有验证 - 2

      当我的预订模型通过rake任务在状态机上转换时,我试图找出如何跳过对ActiveRecord对象的特定实例的验证。我想在reservation.close时跳过所有验证!叫做。希望调用reservation.close!(:validate=>false)之类的东西。仅供引用,我们正在使用https://github.com/pluginaweek/state_machine用于状态机。这是我的预订模型的示例。classReservation["requested","negotiating","approved"])}state_machine:initial=>'requested

    10. ruby - 如何在 Rails 4 中使用表单对象之前的验证回调? - 2

      我有一个服务模型/表及其注册表。在表单中,我几乎拥有服务的所有字段,但我想在验证服务对象之前自动设置其中一些值。示例:--服务Controller#创建Action:defcreate@service=Service.new@service_form=ServiceFormObject.new(@service)@service_form.validate(params[:service_form_object])and@service_form.saverespond_with(@service_form,location:admin_services_path)end在验证@ser

    随机推荐