18

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

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

我目前的做法更务实:

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

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

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

我不习惯这种快速而松散的编程风格,所以也许我错过了一些明显的技巧。还有什么我应该考虑的吗?我相信我很快就会遇到其他问题。

编辑: 这个问题与 DDD 无关。请注意,许多人都在与传统的 DDD 方法作斗争——Seemann 似乎得出了相同的结论Rahien 谈到了“为了抽象反模式而进行无用的抽象”,而 Evans 自己说 DDD 仅在 5% 的应用中真正有用案例。另请参阅此线程. 一些评论/答案可以预见地是关于我如何做 DDD 错误,或者我如何调整我的系统以使其正确。但是,我不是在询问 DDD 或在适合的情况下抨击它,而是我想知道其他人在做什么符合我上面描述的想法。并不是说 DDD 是所有设计弊病的灵丹妙药,每十年都会出现一个新流程(RUP 有人吗?XP、敏捷、Booch 等等……)。DDD 只是最闪亮的新版本,也是最知名和最常用的。但是实用主义应该是第一位的,因为我正在尝试构建可按时发货且易于维护的可销售产品。到目前为止,我学到的最有用的编程公理是 YAGNI。我想要的是将我的系统更改为一种“DDD-lite”,在那里我得到了强大的设计/OOP/模式理念,但没有脂肪。

4

5 回答 5

5

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

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

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

更新

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

于 2012-10-19T23:48:22.483 回答
2

关于这一点的一些想法:

...存储库从不公开 IQueryable ...模型总是在离开存储库后实现...

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

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

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

作为一名 UI 程序员,我将有各种数据要求来使用上面的示例呈现视图:

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

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

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

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

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

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

于 2012-10-21T22:14:03.223 回答
1

当我们不知道您正在构建什么时,谁能给您合理的建议?在宏伟的计划中,您可能正在构建错误的解决方案(不是说您是)。所以要意识到我们所能涉及的只是技术设计问题和类似的过去经历。

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

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

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

总的来说,我有这样的感觉,就是这样,你把事情弄得一团糟,因为你通过依赖不显示所需形状的对象(导航属性为空)来对抗映射摩擦,从而在不同的领域引起问题。

于 2012-10-21T21:23:40.120 回答
0

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

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

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

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

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

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

除了数据/域分离之外,这些东西对我来说似乎都不是从根本上反 DDD 的。

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

于 2012-10-22T12:17:36.050 回答
0

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

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

于 2012-10-20T21:31:21.917 回答