DTO应该由域实体还是持久性生成?

时间:2015-12-28 20:01:06

标签: c# architecture domain-driven-design

对于具有现代ORM的分层应用程序,我常常不确定如何创建特定类以遵守所谓的"最佳实践"同时也注重性能要求。

考虑到应用程序中可能包含以下任意类型的对象:

  1. 域实体 - 这些是包含业务逻辑的丰富类(对吗?),并且根据ORM功能,可能与持久性设计直接相关。

  2. DTO - 这些是更简单的类,可以剥离业务逻辑,以便将数据传递给内部和外部客户端。有时这些是扁平化的,但并非总是如此。

  3. 查看模型 - 这些与DTO类似,因为它们更简单,没有业务逻辑,但它们通常非常扁平,通常包含与之相关的其他位用户界面正在服务。

  4. 我遇到的挑战是,在某些情况下,将域实体或任何面向持久性的类映射到更简单的实体(如DTO或ViewModel)会阻止您进行重要的性能优化。

    例如:

    我们假设我有一些看起来像这样的域名实体:

    public class Event
    {
        public int Id { get; set; }
        public string Name { get; set; }
        public DateTime EventDate { get; set; }
    
        // These would be reference types in most ORMs
        // Pretend in the setter I have logic to ensure the headliner =/= the opener
        public Band Headliner { get; set; }
        public Band Opener { get; set; }
    }
    
    public class Band
    {
        public int Id { get; set; }
        public string Name { get; set; }
        public Genre Genre { get; set; }
    }
    

    在现实世界中,这些可能会复杂得多,有各种业务逻辑,可能还有一些验证调用等。

    如果我公开了一个公共API,我的DTO可能看起来非常像这个例子,没有任何业务逻辑。

    如果我还有一个MVC网络应用程序,我想要显示一个事件列表,我可能想要一个看起来像这样的视图模型:

    public class EventViewModel
    {
        public int Id { get; set; }
        public string Name { get; set; }
        public DateTime EventDate { get; set; }
    
        public int HeadlinerId { get; set; }
        public string HeadlinerName { get; set; }
        public int OpenerId { get; set; }
        public string OpenerName { get; set; }
    }
    

    通常,人们只需使用引用来提取完整​​的域实体,然后使用映射实用程序来水化视图模型。

    但是,我们说我有成千上万的记录。现在,ORM可能会创建一个查询风暴来填充完整的引用对象(这可能比这个示例复杂得多,有自己的引用)。性能开始严重受损并不需要很长时间。

    问题是什么?

    我知道我并不是唯一一个遇到这个问题的人,所以我很想知道人们如何维护分层应用程序,同时仍然需要在生成表示多个对象的同时保持性能相同的基础域信息

    让两个Event - ish对象代表相同的持久化数据感觉不对,但与此同时,持久层似乎不应该知道DTO或视图模型,否则什么是争取分离的重点?

    那你怎么解决这个问题?持久性是否了解域实体的严格,详细表示以及这些实体中数据的轻量级描述?是那些重量较轻的描述DTO还是某些域实体精简版?

8 个答案:

答案 0 :(得分:5)

您的问题没有简单的答案,因为它实际上取决于您希望通过您的架构实现的目标。 这是一种经典的架构权衡。

这也意味着你需要自己决定。确保您了解每种方法的优缺点,然后决定您的项目。以下是优缺点列表:

严格分离的优点

  • 能够根据特定层的职责调整和调整结构。例如,持久性DTO可以以不同于域实体的方式存储数据以支持复杂的查询案例。
  • 支持数据迁移案例的能力。使用单独的持久性DTO,您可以选择加载" old" DTO格式化并将其转换为" new"域实体。
  • 简化返回外部世界的DTO的能力,例如:通过API。这在使用DDD时几乎总是有意义,因为使用DDD通常表明域很复杂。
  • 更好地分离开发人员的顾虑。通常,严格的分层会增加团队并行处理相同功能的可能性,例如:一个在持久性中,一个在域中。
  • 根据ORM或数据库的功能集,在持久性中直接使用域实体甚至不是一个选项。如果它是一个选项,它可能比拥有专用的DTO更复杂。

共享类的优点

  • 使用相同功能的代码较少。
  • 通常可以加快新功能的开发时间。
  • 较小的概念开销。我认为这是一个小问题,因为DTO和视图模型是众所周知的概念,但它可能是一个问题,取决于团队。

如您所见,我不认为性能是共享方法的优势。主要原因是精心设计的对象到对象映射的数量级比从数据库加载数据。因此,我非常确信严格分离方法中的性能问题是由于其他问题,而不是分层。

通过以上几点(可能更多特定于您的环境),您应该能够做出决定。我过去曾使用过这两种方法,但对于一定规模的项目,我总是选择严格的分离方法。

答案 1 :(得分:1)

乔希,

域实体必须独立于ORM,事实上,如果您遵循DDD原则,所有域层都不应该依赖于任何其他层。 DTO只是在层之间传递数据,在大多数情况下,它在Repository的接口中使用,作为方法的返回。作为服务的存储库的接口应保留在域层中。

答案 2 :(得分:0)

  

让两个Event-ish对象代表相同的持久化数据感觉不对,...

实际上,这并不一定是坏事。您EventViewModel班级的Event可能是eventually consistent。您的Event确保符合所有Event业务规则,而EventViewModel可能会通过监听domain events来更新,EventEventViewModelProjection发出类。这有时也称为投放 - Event侦听EventViewModels域事件(无双关语),并在$route['default_controller'] = 'login';上投射。{/ p>

  

但与此同时,持久层似乎不应该知道DTO或视图模型,......

好吧,如果您选择保留DTO并查看模型,那么持久性逻辑应该在某处编码

  

否则,争取分离的重点是什么......那么你如何解决这个问题......那些重量较轻的描述是DTO还是某些领域的实体?

不可能给出一个明确的答案 - 这些都是设计考虑因素,具体取决于您的具体情况。如果遇到性能问题,那么使用像我提到的可以这样的域事件是一个好主意。

您可能有兴趣阅读cqrseventual-consistency以获得一些想法。

答案 3 :(得分:0)

DTO没有行为,它们用于数据传输。

ViewModels包含有关演示文稿的一些行为,因此它们也不是DTO。如果您没有任何特定于视图的行为,则可以在演示文稿中使用DTO。

域实体和ORM实体不相同。你在做什么可能是活跃的记录,而不是域模型。您应该能够用您喜欢的持久性逻辑替换ORM。它们必须分离。

我认为您将DTO与值对象混淆,后者是业务对象,实际上具有行为并且是持久的。如果它没有标识并且它包含属于一起的行为或多个值,那么通常使用值对象描述某些内容。例如,地址,电话号码,ID等可以是值对象。

您无法使用域对象将数据传输到演示文稿。演示文稿必须无法直接访问和修改域对象,这就是我们使用DTO在演示文稿和应用程序服务之间发送数据的原因。应用程序服务可以访问域对象。

答案 4 :(得分:0)

  

通常,人们只需使用引用来提取完整​​的域实体,然后使用映射实用程序来水化视图模型。

     

但是,假设我有成千上万的记录。现在ORM是   可能会创建一个查询风暴来填充完整的引用   对象(可能比这个例子复杂得多,有   他们自己的参考)。性能开始不需要很长时间   严重受苦。

尽量减少查询次数并提高效果:

  1. 在一次通话中请求多个对象(例如,100个事件)

  2. 在一次通话中请求与主要对象相关的对象(例如,100个带有头条新闻和开启者的事件)

  3. 缓存对象以查找已请求的对象,而不是再次请求

  4. 来自ViewModel的队列请求(每个ViewModel告诉它需要哪些对象,然后在一次调用中请求所有对象,并且每个ViewModel都会返回它要求的对象)

  5. 根据您要查询的图层,Object在服务层的上下文中表示DTO,在域/持久层的上下文中表示Entity

答案 5 :(得分:0)

在域驱动设计(DDD)中,域层应该不了解持久性,表示,缓存,日志记录和其他基础结构服务。这可以通过使用抽象(使用接口而不是依赖服务上的具体服务)来实现。您可以应用SOLID原则来帮助您创建良好的软件架构:

  

S是单一责任原则(SRP)
  O代表开放闭合原理(OCP)
  L Liskov替代原则(LSP)
  我接口隔离原则(ISP)
  D依赖注入原则(DIP)

答案 6 :(得分:-1)

我认为这是您开始尝试DDD时出现的第一个问题之一:查询结束显示数据时的性能。

这里的关键概念是域模型必须关注操作,实施业务规则并最终触发事件,而不是提供信息。当然,您仍然可以将其用作向用户显示的数据源,但是,如果您遇到性能问题,最好是评估Command-Query Responsibility Segregation模式(CQRS)的使用情况。

有了它,要显示的数据由另一个模型(特别是数据模型)表示,在您的示例中,可以是EventViewModel类。 数据模型独立于域模型而设计,通常以从数据源构建它的方式设计(即:不需要对象映射)。

答案 7 :(得分:-1)

域实体 代表您的域/业务。例如,在抵押域中,托管账户是域实体。托管帐户由其帐号标识。此实体不代表您的表架构,这对您的数据库一无所知。

public class Escrow
{
public Guid AccountId {get; set;}
public decimal GetBalance()
}

查看模型

我始终将视图模型与Domain和DTO分开,因为我希望View Models代表我的视图而不是其他任何内容。这将有数据注释,验证逻辑等。

DTO和OR​​M实体

现在这是一个棘手的问题。我将DTO和OR​​M实体保留在同一个项目中,并确保它们对任何事物都没有任何依赖关系,它们只是POCO。我首先创建ORM实体,并在需要时添加或创建DTO。

我使用跨层创建的ORM实体,我尽可能将它们用作DTO。我不向这些ORM实体添加或删除属性。如果我需要一个与我的服务的ORM实体或我的应用程序中的任何其他层略有不同的结构,我会为该要求创建新的POCO,并且它们可供所有层使用。

例如,如果我需要一个我想传递给ORM实体中不可用的UI层的计算值(因为它们没有被保留),那么我创建一个只包含必需字段的新POCO。

我使用AutoMapper在对象之间复制数据