一个大的脂肪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来进行业务逻辑计算。

因此,我觉得传递整个OrderDetailDTO对象是不对的(而且很困惑,因为我不确定哪些属性需要有值,哪些没有),尽管它只需要填充其中的2个属性,但我仍然需要传递回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。业务逻辑计算的数量。

您没有发布任何代码,但在注释中有一个合理的点,类可以继承;基类可以具有所有类所具有的公共属性,子类可以具有更多属性,再加上它可以获得基本属性。客户端可以发送一个基类";我想点苹果,3英寸;并得到一个子类";订购项目苹果,3,$1";

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

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

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


在这一切结束时,你可能需要坐下来,首先规划出你的工作流程,以及他们需要什么数据,并在重复使用一个跟踪所有事情的大型dto与为每个案例都有一个dto和最终有1000个dto之间取得平衡。这是两个极端,你几乎永远不会去那里。总有一些重用元素可以而且应该应用于限制维护难题。如果类有合理的公共基元素,可以随意继承它们,但我不建议您有一个具有订单Id的基OrderDto,然后是createorder、updateorder、canceloder、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);
}

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

最新更新