【问题标题】:Gave up DDD, but need some of its benefits放弃 DDD,但需要它的一些好处
【发布时间】:2012-10-10 13:34:36
【问题描述】:

我正在放弃传统的 DDD,这通常会浪费大量时间,并迫使我进行无休止的映射:data layer <--> domain layer <--> presentation layer

即使是很小的更改,我也必须更改数据模型、域模型、表示模型/视图模型,然后是存储库、管理器/服务类,当然还有 AutoMapper 映射,然后测试整个东西!每个调用都需要调用一个层,该层调用一个调用底层代码的层。 除了“你将来可能需要它”之外,我没有得到任何回报。嗯。

我目前的做法比较务实:

  • 我不再担心“数据层”和“域层”之间的区别,因为没有意义 - 术语可以互换。我让 EF 做这件事,并在需要时在顶部添加接口和存储库。
  • 我已经合并了我的“数据”和“域”项目(合并到“核心”,无聊的名字,我知道),我几乎可以发誓 Visual Studio 实际上运行得更快。
  • 我允许 EF 实体在堆栈中上下移动,但是,我仍然像往常一样将它们映射到表示模型/视图模型。
  • 对于简单的操作,我直接从控制器调用存储库,对于复杂的操作,我照常使用域管理器/服务;存储库从不公开 IQueryable。
  • 我将实体/POCO 定义为部分类,因此我可以在相应的部分类中单独添加域行为。

问题:我现在到处使用实体,所以客户端代码可以看到它们的导航属性。并且模型总是在离开存储库后实现,因此这些导航属性通常为空。

可能的解决方案:
1. 忍受它。这很丑陋,但比上面解释的问题更可取。
2.为每个实体定义一个隐藏导航属性的界面;并使客户端代码使用接口。但具有讽刺意味的是,这意味着另一层(尽管很薄且易于管理)。
3. 还有什么?

我不习惯这种又快又松的编程风格,所以我可能遗漏了一些明显的技巧。还有什么我应该考虑的吗?我相信我很快就会遇到其他问题。

编辑: 这个问题与 DDD 无关。请注意,许多人与传统的 DDD 方法作斗争——Seemann appears to 到达 the same conclusionRahien speaks about the "Useless Abstraction For The Sake Of Abstraction Anti Pattern",而 Evans 本人表示 DDD 仅在 5% 的情况下真正有用。还有see this thread。一些 cmets/answers 可以预见地是关于我如何做 DDD 错误,或者我如何调整我的系统以使其正确。但是,我不是在询问 DDD 或在适合的情况下抨击它,而是我想知道其他人在做什么符合我上面描述的想法。并不是说 DDD 是所有设计弊病的灵丹妙药,每十年都会出现一个新流程(RUP 有人吗?XP、敏捷、Booch 等等……)。 DDD 只是最闪亮的新版本,也是最知名和最常用的。但是实用主义应该是第一位的,因为我正在尝试构建可按时发货且易于维护的可销售产品。 到目前为止,我学到的最有用的编程公理是 YAGNI。 我想要的是将我的系统更改为一种“DDD-lite”,在那里我得到了强大的设计/OOP/模式哲学,但没有胖子。

【问题讨论】:

  • 顺便说一句,为什么您在“纯”DDD 方法中有data layer <--> domain layer 映射? DDD 实体应该是 POCO 实体,这使得它们很容易持久化。
  • 这只是偏离了我的主要问题,但要回答:在纯 DDD 或各种 DDD 变体中,域模型是从一个或多个数据模型映射的。数据模型是 EF 实体 POCO,但域模型不是(它包含数据和行为)。可能存在语义对应,但这是一种误导,因为它们不是一回事。
  • 查看 Eric Evans Blue Book 中的“域对象的生命周期”一章。还有一个 NDDDSaple 项目,您可以在其中看到如何实现持久化没有这个额外的映射。
  • 因为没有参考资料而不具建设性? Seemann 的文章"Is Layering Worth the Mapping?" 怎么样?我已经描述了一个其他人也在努力解决的实际问题。
  • 我不同意关闭这个问题并投票重新打开它。 @BobbyB:您可以编辑您的问题并将指向Seemann 文章的链接和简短的草图放入您的问题中。在您觉得您的问题符合常见问题解答中的规则后,您可以将其标记为重新打开。

标签: asp.net-mvc entity-framework domain-driven-design poco


【解决方案1】:

典型的 DDD 持久化方法是将域模型直接映射到相应的表。从技术上讲,映射仍然存在(并且通常在代码中声明),但没有明确的数据模型,正如 lazyberezovsky 所指出的那样。

无论您是否使用 DDD,导航属性的问题都可以通过几种不同的方式解决。我不喜欢方法 1,因为它使你的代码更难推理——你永远不知道哪些属性会被设置,哪些不会。方法 2 在理论上要好得多,因为它使给定查询所需的内容非常显式,并且使事情显式通常是一种很好的做法。一种类似但更简单且不那么脆弱的方法是使用read-models,它们只是旨在满足一组查询的给定查询要求的对象。在 DDD 的上下文中,它们允许您将行为丰富的实体与查询分离,这通常是不一致的。现在DRY 的支持者可能会大喊异端并拿着火把和干草叉向你袭来,但实际上维护读取模型和实体通常要容易得多,然后尝试通过接口或强制实体来满足查询要求复杂的映射策略。另外,读取模型和行为模型的职责有很大不同,因此 DRY 不适用。

这并不是说 DDD 适用于您的场景。避免完全成熟的 DDD 通常是一个明智的决定,尤其是在大多数情况下是 CRUD。谨慎是正确的,KISS and YAGNI 的一个很好的例子。当您的域包含复杂的行为,而不仅仅是数据时,DDD 会受益。无论如何,读取模型模式都适用。

更新

对于不使用读取模型的实现,请查看Fetching Strategy Design,其中获取策略的概念允许准确指定数据库所需的内容,从而缓解导航属性问题。链接帖子中引用的材料也很有趣。总的来说,这试图避免其他方法中存在的间接层。但是,在我看来,使用建议的获取策略比使用读取模型更复杂,而最终结果是相同的。

【讨论】:

  • 读取模型似乎增加了另一层间接性。这正是我试图避免的那种做法。我很欣赏它们在某些场景中占有一席之地 - EF6 显然将对存储过程提供本机支持,这可以使读取模型易于使用。
【解决方案2】:

关于这一点的一些想法:

...存储库永远不会公开 IQueryable ...模型总是 在他们离开存储库后实现...

您的问题被标记为“asp.net-mvc”,因此您有一个 Web 应用程序。 90% 或更多的请求将是 GET 请求,它们应该从数据库中获取一些数据并在 Web 视图中显示这些数据。这些需要的数据真正实体的频率有多高,而不仅仅是属性包(实体类型的属性选择或可能由多个实体的属性组成)?

假设您的应用程序有 100 个视图。其中只有少数会显示完整的实体:

  • 其中 50 个是显示所选数据的列表视图(客户有 ID 和地址,但没有客户的联系人、电话号码和销量)
  • 其中20个包含自动填充文本框以选择参考(订单的客户,但自动填充列表中仅显示客户的姓名和城市,而不是其余的地址或联系人、电话号码和销量并且只显示前 5 个匹配项)
  • 1 是客户的编辑视图,显示所有内容,但不显示销量
  • 1 是客户最近五个订单的详细信息视图
  • 1 是订单的详细信息视图,包括订单项目,包括每个项目的产品,但没有产品的供应商名称
  • 1 是相同的视图,但专门针对希望查看每个项目和项目产品的供应商以及过去三个月平均供应商提前期的采购部门。
  • 1 是服务部门的视图,仅显示产品类别“维修服务”的订单项的订单
  • 人力资源部门的 1 个视图显示员工包括存储为大块的照片
  • 人事计划部门的 1 个视图显示了没有照片的员工的简短版本
  • 等等等等

作为一名 UI 程序员,我需要各种数据才能使用上述示例呈现视图:

  • 我只需要选择一些属性
  • 我什至需要针对不同视图对同一实体的属性进行不同选择
  • 我需要一份包含所有商品但未提及产品的订单
  • 我需要一份包含所有商品(但不是商品的所有属性)并包含对产品和供应商的引用(但不是所有供应商的属性)的订单
  • 我需要一个仅包含经过过滤的订单项列表的订单
  • 我需要一个客户,包括最后五个订单,而不是他曾经拥有的所有 3000 个订单
  • 我需要一名员工,但请不要使用大斑点图像
  • 等等等等

作为数据访问/存储库/服务开发人员,如何满足这些要求?

  • 我只提供了一些方法和实体化实体:加载订单标题,加载订单标题与项目,加载订单标题与项目和产品,加载订单标题与项目和产品和供应商,加载客户标题(抛出 15 20 个属性,亲爱的 UI 开发人员,如果你只需要 5 个属性),加载所有 3000 个订单的客户标头(扔掉 2995,亲爱的 UI 开发人员,如果你只需要 5 个)等等,等等。我从存储库返回接口隐藏未加载的导航属性。
  • 我关心 UI 需要的每一个细节:我创建存储库/服务方法,如 GetFiveCustomerPropertiesForAutoCompleteGetCustomerWithLastFiveOrders 等。我从存储库返回隐藏属性的接口(也是标量)我没有加载。或者我返回包含请求属性的“DTO”。每天,当 UI 开发人员调用下一个视图的数据需求时,我都会更改存储库/服务并创建新的 DTO。
  • 我从存储库返回 IQueryable<TEntity> 并告诉 UI 开发人员“自己创建 LINQ 查询以获取视图所需的数据”。 (第二天早上,DBA 抱怨数百次执行糟糕的数据库查询。)
  • 我从存储库/服务中返回“准备好的”IQueryable<TEntity>s,例如,安全问题,例如为用户的访问权限应用 Where 子句或为搜索词附加 Where 子句或应用NoTracking 查询选项。我告诉 UI 开发人员:“您可以使用 a) 投影 (Select)、b) 分页 (TakeSkip) 以及 c) 排序 (OrderBy) 来扩展查询,因为我认为这些三个查询部分作为 UI 关注点。所有其他查询要求(过滤、加入、分组等)必须在存储库/服务层中实现,并且在 UI 层中被禁止。这里最重要的部分是直接通过 LINQ/SQL 查询实现 ViewModel 的投影,无需中间映射层,也无需加载超出所需列/属性的开销。

这些只是一些想法。每种方法都有其优点和缺点。在至少有一个或几个开发人员了解存储库/服务和 UI/“投影”层中发生的情况的小型团队中工作,根据我的经验,最后一个选项对我来说很好,尽管它并不总是适用描述的严格规则(例如,按产品类别过滤订单中包含的订单项目需要在投影内部(即 UI 层)应用Where 子句)。对于 POST 请求和数据修改,我将使用 DTO 将从视图收集的数据发送回要在该处处理的服务。

为了更严格地分离“查询层”和 UI 层,我可能更喜欢接近第二个选项的东西,可能不是针对每个 UI 需求都有一个接口/DTO,而是以某种方式简化为一组 DTO 来满足最常见的需求(有时会产生一些不必要的加载属性的开销)。但是,由于需要大量必要的存储库/服务方法、(可能很多)DTO 的额外维护以及 DTO 和 ViewModel 之间的中间映射,我希望这比上一个选项工作更多。

就我个人而言,当我 90% 的时间都不需要它们时,我关心实现完整的实体,尤其是复杂的对象图。但我的担忧并没有通过广泛的性能测量得到证实,证明这种方法对于没有特殊高性能需求的“普通”应用程序确实是个问题。

【讨论】:

    【解决方案3】:

    当我们不知道您正在构建什么时,谁能给您中肯的建议?从总体上看,您可能正在构建错误的解决方案(不是说您是)。所以请务必意识到我们所能涉及的只是技术设计问题和类似的过去经历。

    确实,很多人都会遇到您的问题。映射是静态类型领域的松耦合税。也许更动态的语言可以解决你的一些痛苦。或者,您可能会发现更多自动化的优点(DSL、MDA)。您也可以改用客户端服务器。

    接口不是层,而是抽象。明智地使用它们。

    就我个人而言,我永远不会走这些捷径。被咬太多次试图跳过步骤。逻辑开始在奇怪的地方出现。如果我有一个数据驱动的应用程序来开发简单的数据集,我也会想到 EF。但我不称对象为 DDD 意义上的聚合或实体,而只是 ERD 意义上的实体。 Transactionscript 可能比使用部分方法更合适。至于读取模型对象,这些不是间接层。

    总的来说,我有这样的感觉,只是因为你通过依赖不显示所需形状的对象来对抗映射摩擦(导航属性是null) 从而导致不同区域出现问题。

    【讨论】:

    • 不确定我是否完全理解您的建议,但在我看来,您说要简化我的问题并避免走捷径,我应该采用动态输入...似乎是一个悖论某处。顺便说一句,我们的系统设计得非常好,实际上也非常好,带有多余的分层。那是我最初问题的要点。该工具需要解决问题,我认为 DDD 是错误的工具,除非它是我所追求的“精简”版本。我花了同样多的时间从 RUP 群体思维中脱离出来,所以这次我想彻底摆脱困境。
    • “接口不是层” - 从技术上讲,你是对的。这似乎是迄今为止最好的方法。但是我讨厌每次进行更改时都需要同时更改两组项目(即模型及其接口)。 “只需在其上打一个接口”通常会隐藏一些更大的问题。
    • 我并不是说使用动态语言是一种捷径,只是它可能会让你的痛苦更容易忍受。 YMMV。我不知道我的建议是否合理,因为我显然缺乏判断的细节。
    【解决方案4】:

    我会尽量简短——我们采用了方法 2——即,添加您在客户端上使用的接口层。您可以让 EF 为您生成它们,只需稍微调整 .tt 模板即可。

    是的,它(还)创建了另一个层,但它是无逻辑的并且不会增加复杂性。当然,如果您的客户端需要反序列化实体,您必须添加(还)另一个层来处理反序列化并引用实体定义和他将返回给客户端的接口。但它也很薄,所以我们学会了忍受它,因为它工作得很好,而且客户端真的很干净......

    【讨论】:

      【解决方案5】:

      问题:我现在到处都在使用实体,所以客户端代码 可以看到他们的导航属性。

      我不太明白为什么这是一个问题,以及它与 EF 实体的关系如何。 客户端代码您是指表示层代码还是使用您的实体的任何代码?

      对于 UI 代码,一个简单的解决方案是定义不公开这些导航属性的 ViewModel(或者根据您的 GUI 需要的对象图深度只公开其中的几个)。

      对于其他代码,能够看到实体的导航属性是很正常的。他们公开是有原因的。如果你滥用它们,你最终可能会违反 Demeter 法则,但不要落入这个陷阱是开发人员纪律的问题。

      实体包含自己的合约 - 有权访问该实体的所有代码都应该能够使用该合约的任何部分。如果您觉得您的实体暴露太多,并且您需要在它们之上放置接口以限制对某些部分的访问,那么它可能只是一个不同的实体。

      • 我不再担心“数据层”和“域层”之间的区别,因为没有意义 - 术语是
        可互换。我让 EF 做这件事,并添加接口和
        需要时将存储库放在顶部。
      • 我已经合并了我的“数据”和“域”项目(合并为“核心”,我知道这个名字很无聊),我几乎可以发誓 Visual Studio 是
        实际上跑得更快。
      • 我允许 EF 实体在堆栈中上下移动,但我仍然照常将它们映射到表示模型/视图模型。
      • 对于简单的操作,我直接从控制器调用存储库,对于复杂的操作,我使用域管理器/服务作为 通常;存储库从不公开 IQueryable。
      • 我将实体/POCO 定义为部分类,因此我可以在相应的部分类中单独添加域行为。

      这些东西对我来说似乎根本不反对 DDD,除了数据/域分离。

      特别是如果您使用数据库优先 EF -DDD 显然是一种以域为中心的方法,您不应该在定义实体之前定义表。也不清楚您的某些域实体是直接与数据库或 EF 通信(不符合 DDD - 更一般地说,分层架构 - 兼容)还是您系统地在两者之间有数据访问对象(符合 DDD)。

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2012-04-11
        • 2020-07-25
        • 2017-06-16
        • 1970-01-01
        • 2016-04-22
        相关资源
        最近更新 更多