对DTO使用合成和继承


13

我们有一个ASP.NET Web API,它为我们的单页应用程序提供了REST API。我们使用DTO / POCO通过此API传递数据。

现在的问题是,随着时间的推移,这些DTO越来越大,所以现在我们要重构DTO。

我正在寻找如何设计DTO的“最佳实践”:当前,我们有一些小的DTO,它们仅包含值类型字段,例如:

public class UserDto
{
    public int Id { get; set; }

    public string Name { get; set; }
}

其他DTO按组成使用此UserDto,例如:

public class TaskDto
{
    public int Id { get; set; }

    public UserDto AssignedTo { get; set; }
}

另外,还有一些扩展的DTO是通过从其他对象继承来定义的,例如:

public class TaskDetailDto : TaskDto
{
    // some more fields
}

由于一些DTO已用于多个端点/方法(例如GET和PUT),因此随着时间的推移,它们已被某些字段逐步扩展。由于继承和组成,其他DTO也变得更大。

我的问题是现在继承和组成不是好习惯吗?但是当我们不重用它们时,感觉就像是多次编写相同的代码。将DTO用于多个端点/方法是一种不好的做法,还是应该存在仅在某些细微差别上不同的DTO?


6
我不能告诉您您应该怎么做,但是根据我在DTO中工作的经验,继承和合成比起后来更像是一杯劣质咖啡。我将永远不会再使用DTO。曾经 另外,我不认为类似的DTO违反DRY。返回相同表示的两个端点可以重用相同的DTO。返回相似表示的两个端点未返回相同的DTO,因此我为每个端点创建了特定的DTO。如果我被迫选择,从长远来看,构图问题就更少了。
Laiv

@Laiv这是对问题的正确答案,只是不要。不知道为什么要发表评论
TheCatWhisperer

2
@莱夫:你用什么代替?以我的经验,为此而苦苦挣扎的人们只是在想它。DTO只是数据的容器,仅此而已。
罗伯特·哈维

The CatWhisperer,因为我的论点主要是基于观点的。我仍在尝试在我的项目中解决此类问题。@RobertHarvey是的,不知道为什么我倾向于比实际情况更难看。我仍在研究解决方案。我非常相信HAL是解决这些问题的模型,但是阅读您的回答后,我意识到我对DTO所做的工作也非常细致。因此,我将首先实践您的方法。与完全转移到HATEOAS相比,这些变化将不那么引人注目。
Laiv

尝试进行以下讨论,可能对您有所帮助: stackoverflow.com/questions/6297322/…–
约翰尼

Answers:


10

作为最佳实践,请尝试使您的DTO尽可能简洁。只退还您需要退还的物品。只使用您需要使用的东西。如果那意味着一些额外的DTO,那就这样吧。

在您的示例中,任务包含一个用户。那里可能不需要完整的用户对象,可能只是分配了任务的用户的名称。我们不需要其余的用户属性。

假设您想将一项任务重新分配给另一位用户,可能会想在重新分配发布期间传递完整的用户对象和完整的任务对象。但是,真正需要的只是任务ID和用户ID。这些是重新分配任务所需的仅有两条信息,因此要对DTO进行建模。这通常意味着,如果您要争取精益DTO模型,那么每个休息电话都会有一个单独的DTO。

同样,有时可能需要继承/组成。假设有人有工作。一项工作有多个任务。在这种情况下,获得工作也可能会返回该工作的任务列表。因此,没有任何规则禁止合成。这取决于要建模的内容。


尽可能简洁也意味着如果DTO在某些细节上有所不同,我应该重新创建DTO,不是吗?
人员

1
@官员-一般来说,是的。
乔恩·雷诺

2

除非您的系统严格基于CRUD操作,否则您的DTO过于精细。尝试创建体现业务流程或工件的端点。这种方法很好地映射到业务逻辑层,以及Eric Evans的“域驱动设计”。

例如,假设您有一个端点,该端点返回发票的数据,以便可以将其显示在屏幕或表单上并显示给最终用户。在CRUD模型中,您需要对端点进行多次调用以组合必要的信息:名称,账单地址,运输地址,行项目。在业务交易上下文中,来自单个端点的单个DTO可以一次返回所有这些信息。


罗伯特,您的意思是“总根”吗?
约翰尼

当前,我们的DTO旨在提供表单的全部数据,例如,在显示待办事项列表时,TodoListDTO具有TaskDTO列表。但是,由于我们正在重用TaskDTO,因此它们每个都有一个UserDTO。因此,问题在于在后端查询并通过网络发送的大量数据。
人员

是否需要所有数据?
罗伯特·哈维

1

对于像“ DTO”这样的“灵活”或抽象的东西,很难建立最佳实践。从本质上讲,DTO只是用于数据传输的对象,但根据目的地或传输原因,您可能希望应用不同的“最佳实践”。

我建议阅读Martin Fowler撰写的《企业应用程序体系结构的模式》。有整整一章专门讨论模式,DTO在其中获得了非常详细的部分。

最初,它们是“设计”用于昂贵的远程调用的,在那里您可能需要逻辑中不同部分的大量数据。DTO将在一次调用中进行数据传输。

根据作者的说法,DTO并不是要在本地环境中使用的,但是有些人发现了它们的用途。通常,它们用于将来自不同POCO的信息收集到用于GUI,API或不同层的单个实体中。

现在,有了继承,代码重用就更像是继承的副作用,而不是它的主要目标。另一方面,以代码重用为主要目标来实现组合。

有人建议同时使用组合和继承,同时利用两者的优点并尝试减轻其缺点。以下是在选择或创建新的DTO或与此相关的任何新的类/对象时我的思维过程的一部分:

  • 我在同一层或相同上下文中对DTO使用继承。DTO永远不会继承POCO,BLL DTO永远不会继承DAL DTO,等等。
  • 如果我发现自己试图隐藏DTO中的字段,我将进行重构,也许可以使用组合。
  • 如果我只需要基本DTO的几个不同字段,就将它们放在通用DTO中。通用DTO仅在内部使用。
  • 基本的POCO / DTO几乎不会被用于任何逻辑,这样,基本的POCO / DTO只能满足其子代的需求。如果我需要使用基础,则避免添加任何其子代永远不会使用的新字段。

其中一些可能不是“最佳”实践,它们在我一直在从事的项目中效果很好,但是您需要记住,没有一个适合所有的大小。对于通用DTO,您应该小心,我的方法签名如下所示:

public void DoSomething(BaseDTO base) {
    //Some code 
}

如果任何方法都需要自己的DTO,我会继承,通常我唯一需要做的更改就是参数,尽管有时我需要针对特定​​情况进行更深入的研究。

从您的评论中我知道您正在使用嵌套的DTO。如果您的嵌套DTO仅由其他DTO的列表组成,那么我认为最好的办法是解开列表。

根据需要显示或使用的数据量,创建限制数据的新DTO可能是一个好主意。例如,如果您的UserDTO有很多字段,而您只需要1或2,则最好只包含那些字段的DTO。在设计DTO时,定义DTO的层,上下文,用法和实用程序会很有帮助。


我不能在答案中添加超过2个链接,这是有关本地DTO的更多信息。我知道它的信息很旧,但我认为其中一些仍然有用。
IvanGrasp

1

在DTO中使用合成是一种很好的做法。

具体类型的DTO之间的继承是一种不好的做法。

一方面,在像C#这样的语言中,自动实现的属性几乎没有可维护性开销,因此复制它们(我确实讨厌复制)并不像通常那样有害。

不在 DTO之间使用具体继承的原因之一是某些工具会很乐意将它们映射到错误的类型。

例如,如果使用执行class name -> table name推断的Dapper这样的数据库实用程序(我不推荐使用,但它很流行),则很容易最终将派生类型保存为层次结构中某个位置的具体基类型,从而丢失数据甚至更糟。

在DTO之间使用继承的更深层原因是,不应将其用于在没有明显“是”关系的类型之间共享实现。在我看来,TaskDetail听起来不像是的子类型Task。可能很容易是的属性,Task或者更糟的是的超型Task

现在,您可能要担心的一件事是保持各种相关DTO的属性名称类型之间的一致性。

因此,虽然具体类型的继承将有助于确保这种一致性,但是最好使用接口(或C ++中的纯虚拟基类)来保持这种一致性。

考虑以下DTO

interface IIdentity
{
    int Id { get; set; }
}

interface INamed
{
    string Name { get; set; }
}

public class UserDto: IIdentity, INamed
{
    public int Id { get; set; }

    public string Name { get; set; }

    // User specific properties
}

public class TaskDto: IIdentity
{
    public int Id { get; set; }

    // Task specific properties
}
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.