【问题标题】:One big fat DTO vs multiple skinny DTO [closed]一个大胖 DTO 与多个瘦 DTO [关闭]
【发布时间】:2020-12-12 17:39:56
【问题描述】:

我一直在为我的应用程序的命名约定和设计模式而苦苦挣扎,很难保持一致,所以我有一个简单的案例,假设我有一个方法名为 CreateOrder 的服务

public OrderDTO CreateOrder(int customerID, OrderShipDTO shipping, OrderDetailDTO products);

这是 DTO 类

    public class OrderDTO
    {
        public int ID { get; set; }
        public decimal PriceTotal { get; set; }
        public string Status { get; set; }
        public string PaymentMethod { get; set; }

        public OrderShipDTO OrderShip { get; set; }
        public ICollection<OrderDetailDTO> OrderDetails { get; set; }
    }
    public class OrderShipDTO
    {
        public string Name { get; set; }
        public string Phone { get; set; }
        public string Address { get; set; }
        public string Province { get; set; }
        public string City { get; set; }
        public string District { get; set; }
        public string SubDistrict { get; set; }
        public string ZipCode { get; set; }
    }

    public class OrderDetailDTO
    {
        public int ID { get; set; }
        public decimal Quantity { get; set; }
        public decimal Price { get; set; }
        public int ProductID { get; set; }
    }

如您所见,我的方法CreateOrder将返回OrderDTO并接受OrderDetailDTO参数,但该方法实际上只需要属性OrderDetailDTO.ProductIDOrderDetailDTO.Quantity进行业务逻辑计算。

所以,我觉得传递整个 OrderDetailDTO 对象并不适合(并且令人困惑,因为我不确定哪些属性需要有值,哪些不需要),即使它只需要 2 个属性要填写,但我仍然需要传回 OrderDTO,其中将包括 ICollection&lt;OrderDetailDTO&gt;,因为我需要获取 OrderDetailDTO.Price 值并将其展示给我的客户。

所以我正在考虑创建另一个这样的 DTO

    public class OrderDetailDTO_2 //temp name
    {
        public decimal Quantity { get; set; }
        public int ProductID { get; set; }
    }

但是我最终会得到很多 DTO,即使我对它很好,DTO 命名的最佳实践是什么?

【问题讨论】:

  • 您是否愿意将 OrderDetailDTO_2 作为 OrderDetailDTO 的抽象类? & 订单详情更改为public ICollection&lt;OrderDetailDTO_2&gt; OrderDetails { get; set; }?

标签: c# design-patterns dto data-transfer-objects


【解决方案1】:

我没有看到在 orderdetail 中包含 ID 和 ProductId 的意义(从发布的信息中),因为 ProductId 应该足够了;如果用户添加了 2 个苹果,然后添加了 3 个苹果,您可以将单个苹果行的订单数量设置为 5 个苹果,而不是明确跟踪 2 个苹果与 3 个苹果行

我看不出订单详情中没有价格的意义;物品总是有价格的。它可能会改变或不会改变,但每次来回将价格传达给前端并没有什么坏处 - 前端不必记住任何东西,导致我的下一点:

我也看不出订单中的总数有什么意义,除非它与所有详细数量 * 价格的总和有所不同。客户端可以像服务器一样进行求和。如果客户知道数量和价格,让他自己计算总数

我认为 OrderShip 的命名并不好。 OrderShip 实际上是一个地址,看起来它可以在程序的其他部分有很多用途,例如帐单地址、发票地址、通信地址。根据对象的而不是它们的用途命名您的对象 - 使用变量的名称来指示它的用途:

public AddressDto ShippingAddress ...
public AddressDto BillingAddress ...

我正在考虑创建另一个这样的 DTO

永远不要创建一个类名并在其上打一个 2,“因为你想不出更好的名字”——这绝对是零帮助让另一个开发人员(或者当你忘记这一点时你自己项目)知道区别。举起手来告诉我oracle sql VARCHAR和VARCHAR2之间的每一个区别而无需点击手册(戈登,这个是给其他人的;))

该方法实际上只需要属性 OrderDetailDTO.ProductID 和 OrderDetailDTO.Quantity 进行业务逻辑计算。

您没有发布任何代码,但类可以继承的 cmets 中有一个合理的点;基类可以有所有类的共同属性,子类可以有更多属性,加上它得到基类。客户端可以发送一个基类“我想订购苹果,3”并获得一个子类“订购商品苹果,3,1 美元”

我不确定这些论点是否保证创建一个全新的类只是为了省去 1 个属性。我只是重用同一个类,有时它的价格被填写(服务器到客户端),有时不需要/不必被/忽略(客户端到服务器,不希望客户端设置价格!)

所以我有一个简单的案例,假设我有一个名为 CreateOrder 的服务

创建订单并不是我所说的简单案例;如果是这样,它就不需要一个 SO 问题 - 您的创建订单方法似乎没有要求任何与付款相关的参数,但订单 dto 跟踪它,所以我想知道是否错过了?这也是我希望在最后完成的事情,除非您可能允许客户逐渐构建多个购物篮,并且在开始时会生成 orderid 作为购物篮参考(在这种情况下,可能有一个单独的添加过程付款信息)


在所有这一切结束时,您可能需要先坐下来规划您的工作流程以及他们需要哪些数据,并力求在重用一个跟踪所有内容的大型 dto 与为每个案例都拥有一个 dto 之间取得平衡,最终得到 1000 个 DTO。这是两个极端,你几乎永远不会去那里。总有一些重用元素可以而且应该用于限制维护问题。如果类具有合理的通用基本元素,请随意继承类,但我不建议您使用带有订单 ID 的基本 OrderDto,然后是用于 createorder、updateorder、cancelorder、addordershippingaddress、changeordershippingaddress、changeorderbillingaddress、reportorderreceived、reportordernotreceived 等的 DTO - 大多数这些操作中只需要一个订单 ID,可能是地址详细信息或订单详细信息;你可以有几个 dto;基本订单(用于取消、收到报告等操作)和完整订单(如果您要更改送货地址,则账单地址为空)。

您可以使用被调用方法的名称来了解您需要更改的帐单或运费,或者客户端可以发送一对修改后的地址(两个地址都需要更新),修改和未修改的地址(更新一个地址但不更新另一个地址),一个空地址和非空地址(删除一个地址但不删除另一个地址)......服务器可以以相同的方式处理它们:新地址对是事实;用新数据覆盖旧数据。

这在 dto 极端之间取得了平衡;如果我们遇到两种 dto 都不适用的情况,那么如果现有的没有一个是理想的,我们可以考虑再做一个。我们不应该将字段用于其他目的(如果他们通过银行转帐付款,则不要使用信用卡号码字段来存储电话号码),即使它“只是那一次” - 也许添加电话号码将是扩展地址 dto 并将其称为不同的名称的机会,例如 ContactDetail

【讨论】:

    【解决方案2】:

    首先,您必须了解您的业务逻辑和所需的业务实体。在不了解应用程序业务的任何细节的情况下,业务逻辑始终可以被视为一个黑匣子,它需要输入、处理此输入并产生输出,即结果(IPO 模型)。

    考虑到 IPO 模型,您现在可以围绕业务逻辑设计业务实体。识别业务流程,导出逻辑并设计所需的实体(它们的行为或属性和关系)。开始设计接口来描述这些实体。

    请注意,我们的目标并不是为每个方法都有一个专用对象,以便每个对象只公开该方法所需的数据。尽管通过封装数据上下文或职责,接口隔离是实现这一目标的好方法。

    由于您已经表达了对命名约定的关注,还请注意,在描述其物理(例如bool)或逻辑(例如 DTO)数据类型的类型名称中添加前缀或后缀没有任何价值。这种命名风格绝对没有价值。它只会使名称变得丑陋并降低可读性。这就是为什么匈牙利符号在今天已经过时的主要原因。

    示例

    我假设您的业务逻辑的目标是根据用户提供的数据输入创建实际订单。这个例子当然是基于一个非常简化的过程。
    您始终可以使用接口隔离来封装职责。实现细粒度接口的对象,可以通过该接口传递,允许细粒度上下文相关的属性展示。

    数据输入业务实体

    interface IItem
    {
      decimal Quantity { get; set; }
      int ProductID { get; set; }
    }
    
    // Describes data needed for shipping related to a customer
    interface IShippingAddress
    {
      string Address { get; set; }
      string Province { get; set; }
      string City { get; set; }
      string District { get; set; }
      string SubDistrict { get; set; }
      string ZipCode { get; set; }
    }
    
    // Consolidates data needed to create an order. 
    // Provided by customer order process.
    interface IPurchase
    {
      int CustomerId { get; set; }
      IShippingAddress ShippingAddress { get; set; }
      IEnumerable<IItem> Items { get; set; }
    }
    

    数据输出业务实体

    // Result entity that extends IITem to add internal details like price.
    interface IOrderItem : IItem
    {
      int Id { get; set; }
      decimal Price { get; set; }
    }
    
    // Encapsulates contact details
    interface ICustomerContactInfo
    {
      string Phone { get; set; }
      string EmailAddress { get; set; }
    }
    
    // Encapsulates customer details like IsVip, or contact info etc
    interface ICustomerInfo : ICustomerContactInfo
    {
      int CustomerId { get; set; }
      string Name { get; set; }
    }
    
    // Consolidates the data needed for the shipping process
    interface IShippingInfo : ICustomerInfo, IShippingAddress
    {
    }    
    
    // Consolidates and adds data needed for managing the order's delivery process
    interface IDelivery : IShippingInfo 
    {
      bool IsInDelivery { get; set; }
      DateTime PickupDate { get; set; }
      DateTime DueDate { get; set; }
    }
    
    // The result entity
    interface IOrder
    {
      int Id { get; set; }
      decimal PriceTotal { get; set; }
      string Status { get; set; }
      string PaymentMethod { get; set; }
    
      IDelivery DeliveryInfo { get; set; }
      ICollection<IOrderItem> Items { get; set; }
    }
    

    业务逻辑

    public IOrder CreateOrder(IPurchase purchase)
    {
      // Get the result item info that contains actual price etc
      ICollection<IOrderItem> orderItems = GetOrderItems(purchase.Items);
    
      ICustomerInfo customer = GetCustomer(purchase.CustomerId);
    
      IShippingInfo shippingInfo = CreateShippingInfo(purchase.ShippingAddress, customer);
    
      IOrder result = CreateOrderItem(orderItems, shippingInfo);
      return result;
    }
    
    public void SendConfimationMail(ICustomerContactInfo contactInfo)
    {  
      SendMailTo(contactInfo.EmailAddress, message);
    }
    
    public void OrderDeliveryService(IShippingInfo shippingInfo)
    {  
      SubmitDeliveryOrder(shippingInfo);
    }
    

    示例

    public static Main()
    {
      IPurchase purchase = CollectOrderDataFromUser();
      IOrder orderItem = CreateOrder(purchase);
     
      // Only pass the ICustomerContactInfo part of IDelivery as argument
      SendConfimationMail(orderItem.DeliveryInfo);
    
      // Only pass the IShippingInfo part of IDelivery as argument
      OrderDeliveryService(orderItem.DeliveryInfo);
    }
    

    隔离程度或界面设计可能对您没有多大意义。但是这个例子的目的是提供一个关于如何设计业务实体的原始和基本的例子,它反映了业务逻辑,反映了现实生活中的业务流程。它展示了接口隔离如何帮助传递对象的特定上下文(接口)以强制封装或限制贪婪。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2016-07-10
      • 2021-01-21
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2018-11-06
      • 1970-01-01
      相关资源
      最近更新 更多