一个大胖DTO与多个瘦DTO

如何解决一个大胖DTO与多个瘦DTO

我一直在为应用程序争用命名约定和设计模式,很难保持一致性,因此我有一个简单的案例,假设我有一个名为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用于业务逻辑计算。

因此,我感觉不合适(即使我只需要2个值,因为我不确定哪个属性需要一个值,而哪个属性不需要)来传递整个OrderDetailDTO对象要填充的属性,但是我仍然需要传递回OrderDTO并将包含ICollection<OrderDetailDTO>的原因,因为我需要获取OrderDetailDTO.Price值并将其显示给客户。

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

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

但是我最终会遇到很多DTO,即使我对此表示满意,但DTO命名的最佳实践是什么?

解决方法

我看不出在订单详细信息中具有ID和ProductId的意义(因为发布的信息),因为ProductId应该足够;如果用户先添加2个苹果,然后再添加3个苹果,则只需将单个苹果行的订单数量设置为5个苹果,而不用分别跟踪2个苹果对3个苹果行

我看不出在订单明细中没有价格的意义;物品总是有价的。它可能会更改,也可能不会更改,但是在前后交流价格与前端之间没有任何害处-前端不必记住任何内容,从而得出我的下一个观点:

我也看不出要有总价的意义,除非总价与所有明细数量*价格的总和有所不同。客户端可以像服务器一样进行总和。如果客户知道数量和价格,可以自己算出总金额

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

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

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

永远不要创建一个类名,而只是在其上打个“ 2”,因为您无法想到一个更好的名称。在让另一个开发人员(或您自己忘记了这个名称时,这绝对是零帮助)项目)知道区别。举起任何可以告诉我oracle sql VARCHAR和VARCHAR2之间的差异的人,而无需触及手册(Gordon,这是给其他人的东西;))

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

您没有发布任何代码,但是注释中有一个合理的观点,类可以继承;基类可以具有所有类都具有的公共属性,子类可以具有更多属性,并且可以获取基类。客户端可以发送基类“我要订购苹果,3”,并获得子类“订购项目苹果,3,$ 1”

我不确定这些参数是否值得创建一个全新的类,以免除1个属性。我只是重复使用同一类,有时填充了它的价格(从服务器到客户端),有时没有/不必/被忽略(客户端到服务器,不希望客户端设置价格!)

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

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


在所有这些操作的最后,您可能需要首先坐下来规划工作流程以及他们需要什么数据,并努力在重用一个跟踪数据的大型dto与针对每种情况使用一个dto之间取得平衡。最终有1000个DTO。这是两个极端,您几乎永远不会去那里。总有一些重用元素可以并且应该用来限制维护的麻烦。如果它们具有合理的通用基本元素,则可以随意继承类,但是我不建议您使用具有订单ID的基本OrderDto,然后是createorder,updateorder,cancelorder,addordershippingaddress,changeordershippingaddress,changeorderbillingaddress,reportorderreceived,reportordernotreceived等的DTO。这些操作中仅需要一个订单ID,也许是地址详细信息或订单详细信息;您可以有几个dto;基本订单(用于取消,收到报告等操作)和完整订单(如果更改送货地址,则帐单地址为空)。

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

这在dto极端之间取得了平衡;如果碰巧没有dto的情况发生,那么如果没有一个理想的选择,我们可以考虑再做一个。即使是“一次”,我们也不应将字段用于其他用途(不要使用信用卡号字段来存储电话号码(如果他们是通过银行转帐付款的人))-也许添加一个电话号码将是一个将Address dto扩展并命名为ContactDetail

之类的机会。 ,

首先,您必须了解您的业务逻辑和所需的业务实体。在不了解有关应用程序业务的任何详细信息的情况下,业务逻辑始终可以被视为黑匣子,这需要输入,处理该输入并产生输出,即结果(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);
}

隔离的程度或接口的设计可能对您没有多大意义。但是,此示例的目的是提供有关如何设计业务实体的原始示例,这些实体反映了业务逻辑,而业务逻辑又反映了现实生活中的业务流程。它显示了接口隔离如何帮助仅绕过对象的特定上下文(接口)来实施封装或限制贪婪性。

版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 dio@foxmail.com 举报,一经查实,本站将立刻删除。

相关推荐


依赖报错 idea导入项目后依赖报错,解决方案:https://blog.csdn.net/weixin_42420249/article/details/81191861 依赖版本报错:更换其他版本 无法下载依赖可参考:https://blog.csdn.net/weixin_42628809/a
错误1:代码生成器依赖和mybatis依赖冲突 启动项目时报错如下 2021-12-03 13:33:33.927 ERROR 7228 [ main] o.s.b.d.LoggingFailureAnalysisReporter : *************************** APPL
错误1:gradle项目控制台输出为乱码 # 解决方案:https://blog.csdn.net/weixin_43501566/article/details/112482302 # 在gradle-wrapper.properties 添加以下内容 org.gradle.jvmargs=-Df
错误还原:在查询的过程中,传入的workType为0时,该条件不起作用 &lt;select id=&quot;xxx&quot;&gt; SELECT di.id, di.name, di.work_type, di.updated... &lt;where&gt; &lt;if test=&qu
报错如下,gcc版本太低 ^ server.c:5346:31: 错误:‘struct redisServer’没有名为‘server_cpulist’的成员 redisSetCpuAffinity(server.server_cpulist); ^ server.c: 在函数‘hasActiveC
解决方案1 1、改项目中.idea/workspace.xml配置文件,增加dynamic.classpath参数 2、搜索PropertiesComponent,添加如下 &lt;property name=&quot;dynamic.classpath&quot; value=&quot;tru
删除根组件app.vue中的默认代码后报错:Module Error (from ./node_modules/eslint-loader/index.js): 解决方案:关闭ESlint代码检测,在项目根目录创建vue.config.js,在文件中添加 module.exports = { lin
查看spark默认的python版本 [root@master day27]# pyspark /home/software/spark-2.3.4-bin-hadoop2.7/conf/spark-env.sh: line 2: /usr/local/hadoop/bin/hadoop: No s
使用本地python环境可以成功执行 import pandas as pd import matplotlib.pyplot as plt # 设置字体 plt.rcParams[&#39;font.sans-serif&#39;] = [&#39;SimHei&#39;] # 能正确显示负号 p
错误1:Request method ‘DELETE‘ not supported 错误还原:controller层有一个接口,访问该接口时报错:Request method ‘DELETE‘ not supported 错误原因:没有接收到前端传入的参数,修改为如下 参考 错误2:cannot r
错误1:启动docker镜像时报错:Error response from daemon: driver failed programming external connectivity on endpoint quirky_allen 解决方法:重启docker -&gt; systemctl r
错误1:private field ‘xxx‘ is never assigned 按Altʾnter快捷键,选择第2项 参考:https://blog.csdn.net/shi_hong_fei_hei/article/details/88814070 错误2:启动时报错,不能找到主启动类 #
报错如下,通过源不能下载,最后警告pip需升级版本 Requirement already satisfied: pip in c:\users\ychen\appdata\local\programs\python\python310\lib\site-packages (22.0.4) Coll
错误1:maven打包报错 错误还原:使用maven打包项目时报错如下 [ERROR] Failed to execute goal org.apache.maven.plugins:maven-resources-plugin:3.2.0:resources (default-resources)
错误1:服务调用时报错 服务消费者模块assess通过openFeign调用服务提供者模块hires 如下为服务提供者模块hires的控制层接口 @RestController @RequestMapping(&quot;/hires&quot;) public class FeignControl
错误1:运行项目后报如下错误 解决方案 报错2:Failed to execute goal org.apache.maven.plugins:maven-compiler-plugin:3.8.1:compile (default-compile) on project sb 解决方案:在pom.
参考 错误原因 过滤器或拦截器在生效时,redisTemplate还没有注入 解决方案:在注入容器时就生效 @Component //项目运行时就注入Spring容器 public class RedisBean { @Resource private RedisTemplate&lt;String
使用vite构建项目报错 C:\Users\ychen\work&gt;npm init @vitejs/app @vitejs/create-app is deprecated, use npm init vite instead C:\Users\ychen\AppData\Local\npm-