【问题标题】:Should a DTO be generated by a domain entity or from persistence?DTO 应该由域实体生成还是由持久性生成?
【发布时间】:2016-04-02 16:20:46
【问题描述】:

当涉及到具有现代 ORM 的分层应用程序时,我经常不确定应该如何创建特定类以遵守所谓的“最佳实践”,同时还要注意性能要求。

假设您在应用程序中可能有任意数量的以下类型的对象:

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

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

  3. 视图模型 - 这些类似于 DTO,因为它们更简单且没有业务逻辑,但它们通常非常扁平,并且通常包含与它们所服务的 UI 相关的额外位.

我面临的挑战是,在某些情况下,将域实体或任何面向持久性的类映射到更简单的实体(如 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 Web 应用程序,我可能想要一个看起来像这样的视图模型:

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 还是一些域实体 lite?

【问题讨论】:

  • 你的问题是你实际上并没有做 DDD ,你正在做 db/orm 驱动的代码设计。 DDD 是关于高级设计的,它与特定的代码实现无关。很有可能您的“域”模型建模不正确,现在您认为您的应用程序很复杂/由于“DDD”而存在问题。但是很少有开发者真正在做 DDD,大多数都在做和以前一样的事情,但是使用 DDD 词,结果令人厌恶。 CQRS 是性能的答案,但首先您需要一个适当的领域模型,然后了解 CQRS 本身是一种设计原则,而不是一些代码实现。
  • 为了让每个人都简单 CQRS = 将改变业务状态的模型与不改变业务状态的模型分开。而已。没有花哨的图表,没有复杂的编码配方。它只是在设计时问自己:“这个动作会改变(持久的)状态吗?或者我只需要获取一些数据,而不需要改变任何东西”
  • @MikeSW,完全同意我不清楚 DDD / 实施断开连接。问题是我在现实世界的场景中没有看到太多解释它的地方,在那里你有有形的 UI 或 API 输出。客户/订单/项目的 DDD 示例数不胜数,但很少有 应用程序 示例在持久性、查询/报告和 UI 的上下文中显示它。所以我最后说,“这似乎与 X 教条不符……这很糟糕吗??”那么...知道在具有真实业务规则的真实类型的应用程序中适当的域建模的任何好的例子吗?
  • 事情是这样的:DDD 是一个过程,理解它的唯一方法是练习直到你“明白”:) 或参加有人解释事情的研讨会。在写作中很难(这就是弗农的书有 900 页的原因)来解释一个思考过程,而且显然一个 SO 答案的作用甚至更少。只看代码是没有用的,因为代码始终是流程的结果,并且代码是针对手头的问题量身定制的。而且我看到您仍然过于“耦合”到持久性或 UI 等细节。它们很重要,但 CQ(R)S 确实解决了所有问题 :)

标签: c# architecture domain-driven-design


【解决方案1】:

您的问题没有简单的答案,因为这实际上取决于您想通过您的架构实现什么。 这是一个经典的架构权衡。

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

严格分离的优点

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

共享类的优点

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

如您所见,我不认为共享方法的性能优势。主要原因是设计良好的对象到对象映射比加载来自数据库的数据。所以我很有信心,严格分离方法中的性能问题是由其他问题引起的,而不是由于分层。

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

【讨论】:

  • 我认为我的困难在于使用 DDD 来适应复杂的域几乎必然会使持久性具有挑战性。如果复杂对象本身没有持久化,而是投影到 DTO 上进行持久化,那么您最终会遇到潜在的映射挑战(即 AutoMapper 等人无法适应的挑战)。而且似乎大多数示例都非常简化,您无法从它们那里真正了解这些类型的挑战。或者,在某种复杂程度下,根本就没有“最佳实践”。
  • 我不明白为什么映射会很困难,我曾在具有非常复杂域的不同项目中工作过,这从来都不是问题。
【解决方案2】:

乔希,

Domain Entities 必须独立于 ORM,事实上,如果你遵循 DDD 原则,所有的 Domain Layer 都不应该依赖于任何其他层。 DTO 只是在层之间传输数据,在大多数情况下,它用于 Repository 的接口中,作为方法的返回。 Repository 的接口,作为 Service 的,应该留在 Domain 层。

【讨论】:

  • 挑战在于,我不确定如此严格地遵守 DDD 是否值得为完成延迟加载等实际要求所花费的额外工作。如果域没有 ORM 的某些部分,我还没有看到一个好的方法。
  • 域不应该有ORM的一部分,但是ORM应该知道域,这样才能在你的域实体和你的数据库表之间建立映射。为什么你认为你需要在域中引用 ORM?例如,如果您认为需要使用注释来配置表,则不需要。在这种情况下,您可以在数据层中使用 FluentApi,等等。
  • 假设我有一个包含子实体集合的实体,但我不想在需要它们之前从数据库中加载这些实体。父实体要么需要利用 ORM 的延迟加载功能,要么必须在延迟加载中进行烘焙,这意味着必须直接或通过服务层调用 ORM(这两者似乎都不值得工作)。无论哪种情况,仍然存在持久性是否应该创建域实体、DTO 或两者兼而有之的问题。
  • 您将使用哪个 ORM,我不确定 NHibernate,但在实体框架中,为了允许延迟加载,您必须只使您的属性集合虚拟化,例如:public virtual IList Persons { 得到;放; } 。当您查询数据库对象时,它只会在您需要时加载人员,例如:var fewPersons = SomeObject.Persons.Where(p => p.Id = id);并且你总是应该只获得你需要的数据,例如,如果你有一个具有 30 个属性的实体,但在某些情况下你只向用户显示名称和年龄,在这种情况下你应该只返回一个 DTO。
  • 不是真的,但我快到了。某种混合 CQRS-ish 方法可能是路线。
【解决方案3】:

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

其实,这不一定是坏事。你的EventViewModel 可能是eventually consistent 和你的Event 类。您的Event 确保满足所有Event 业务规则,而您的EventViewModel 可以通过收听domain events 来更新,由(例如)Event 类发出。这有时称为投影——EventViewModelProjection 监听 Event 域事件(没有双关语)并将这些事件投影到 EventViewModels

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

好吧,如果您选择持久化 DTO 和视图模型,那么持久化逻辑应该在某处进行编码。

否则,争取分离的意义何在……那么你如何解决这个问题……那些轻量级的描述是 DTO 还是某些域实体 lite?

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

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

【讨论】:

  • 我认为这不能回答问题,CQRS 与问题中描述的问题无关。为这样的事情切换到另一种架构风格似乎是一种过度反应。没有反对 CQRS 本身,但似乎每个人都建议将 CQRS 作为目前所有事物的解决方案。
【解决方案4】:

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

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

查看模型

我总是将视图模型与域和 DTO 分开,因为我希望视图模型代表我的视图而不是其他任何东西。这将有数据注释、验证逻辑等。

DTO 和 ORM 实体

现在这是棘手的问题。我将 DTO 和 ORM 实体保存在同一个项目中,并确保它们对任何东西没有任何依赖关系,它们只是 POCO。我从创建 ORM 实体开始,并在需要时添加或创建 DTO。

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

例如,如果我需要一个计算值并将其传递给在 ORM 实体中不可用的 UI 层(因为它们没有持久化),那么我将创建一个仅包含必填字段的新 POCO。

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

【讨论】:

    【解决方案5】:

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

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

    域实体和 ORM 实体是不一样的。您正在做的可能是活动记录而不是域模型。您应该能够用您喜欢的持久性逻辑替换 ORM。它们必须解耦。

    我认为您将 DTO 与值对象混淆了,值对象是业务对象并且确实具有行为并且是持久的。如果它没有标识并且它包含属于一起的行为或多个值,则通常使用值对象来描述它。例如地址、电话号码、id等都可以是值对象。

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

    【讨论】:

      【解决方案6】:

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

      但是,假设我有数万条记录。现在 ORM 是 可能会创建一个查询风暴来填充完整的参考 对象(这可能比这个例子复杂得多,与 他们自己的参考)。表演开始不久 遭受严重的痛苦。

      尽量减少查询次数并提高性能:

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

      2. 在一次调用中请求具有主对象的相关对象(例如,100 个带有 Headliner 和 Opener 的事件)

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

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

      根据您查询的层,Object 表示服务层上下文中的DTO 或域/持久层上下文中的Entity

      【讨论】:

        【解决方案7】:

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

        这里的关键概念是域模型必须专注于操作、执行业务规则并最终触发事件,而不是提供信息。当然,您仍然可以将其用作向用户显示的数据源,但是,如果您遇到性能问题,最好评估一下命令-查询职责分离模式 (CQRS) 的使用。

        使用它,要显示的数据由另一个模型(特别是数据模型)表示,在您的示例中,它可以是 EventViewModel 类。 数据模型的设计独立于领域模型,并且通常设计为从数据源构建它是高性能的(即:不需要对象映射)。

        【讨论】:

        • 有趣的是你提出了 CQRS。这是我认为可能的解决方案。我的结论是完整的 CQRS 对我的需求来说太过分了,但其中一些概念可能值得集成到应用程序中。基本上就像你说的那样维护两组模型——域模型和快速、扁平的查询模型。例如,我还没有想出如何将它融入 REST API,至少在没有为同一个基本概念实体显示多个资源的情况下是这样。
        • 如果我正确理解您的疑问,我认为您应该独立于数据的结构来设计您的 RESTful API。一旦您决定了资源的一种(或多个,为什么不呢?)表示,您必须将数据映射到该表示,通过方便地设计您使用的查询,可能会将大部分工作卸载到数据库中。
        【解决方案8】:

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

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

        【讨论】:

          猜你喜欢
          • 2017-08-06
          • 2013-02-19
          • 1970-01-01
          • 2011-09-29
          • 2014-10-05
          • 2014-12-08
          • 2020-05-18
          • 1970-01-01
          • 1970-01-01
          相关资源
          最近更新 更多