域/持久性模型隔离通常会很尴尬吗?


12

我深入研究了域驱动设计(DDD)的概念,发现一些原则很奇怪,尤其是在域和持久性模型的隔离方面。这是我的基本理解:

  1. 应用程序层(提供功能集)上的服务从其执行功能所需的存储库中请求域对象。
  2. 该存储库的具体实现从为其实现的存储中获取数据
  3. 该服务告诉封装业务逻辑的域对象执行某些修改其状态的任务。
  4. 该服务告诉存储库保留修改后的域对象。
  5. 存储库需要将域对象映射回存储中的相应表示形式。

流图

现在,鉴于以上假设,以下内容似乎很尴尬:

广告2:

域模型似乎加载了整个域对象(包括所有字段和引用),即使请求它的功能不需要它们。如果引用了其他域对象,则甚至可能根本无法完全加载,除非您同时加载这些域对象以及它们依次引用的所有对象,依此类推。想到了延迟加载,但是这意味着您开始查询域对象,这首先应该是存储库的责任。

鉴于此问题,加载域对象的“正确”方式似乎具有针对每种用例的专用加载功能。这些专用功能将仅加载其设计用例所需的数据。尴尬在这里发挥了作用:首先,我必须为存储库的每个实现维护大量的加载函数,并且域对象最终将null以其字段中携带的不完整状态结束。从技术上讲,后者应该不是问题,因为如果未加载值,则无论如何要求它的功能都不应要求该值。仍然很尴尬和潜在的危害。

广告3 .:

如果没有存储库的任何概念,领域对象将如何验证唯一性对构造的约束?例如,如果我想创建一个User具有唯一社会保险号(已给出)的新保险,则只有在数据库中定义了唯一性约束的情况下,最早的冲突将在要求存储库保存对象时发生。否则,User在创建新的社会保障之前,我可以寻找具有给定社会保障的a并报告错误(如果存在)。但是,约束检查将存在于服务中,而不存在于它们所属的域对象中。我刚刚意识到,域对象被很好地允许使用(注入)存储库进行验证。

广告5:

与让域对象直接修改底层数据相比,我认为将域对象映射到存储后端是一项工作量很大的过程。当然,将具体的存储实现与域代码脱钩是必不可少的前提。但是,确实付出了这么高的代价吗?

您显然可以选择使用ORM工具为您执行映射。这些通常需要您根据ORM的限制来设计域模型,或者甚至引入从域到基础结构层的依赖关系(例如,通过在域对象中使用ORM批注)。我也读过ORM引入了大量的计算开销。

对于NoSQL数据库(几乎不存在任何类似于ORM的概念),您如何跟踪域模型中的哪些属性发生了变化save()

编辑:另外,为了使存储库访问域对象的状态(即每个字段的值),域对象需要显示其内部状态,这会破坏封装。

一般来说:

  • 交易逻辑将流向何方?这当然是持久性特定的。一些存储基础架构甚至可能根本不支持事务(例如内存中的模拟存储库)。
  • 对于修改多个对象的批量操作,我是否必须分别加载,修改和存储每个对象才能通过对象的封装验证逻辑?这与直接对数据库执行单个查询相反。

我希望对此主题进行一些澄清。我的假设正确吗?如果没有,解决这些问题的正确方法是什么?


1
好的观点和问题,我也对此感兴趣。旁注-如果您正确建模了聚合,这意味着在存在的任何给定时间,聚合实例必须处于有效状态-这是聚合的要点(而不是将聚合用作组合容器)。这也意味着,为了从数据库数据恢复聚合,存储库本身通常必须使用特定的构造函数和一组突变操作,而且我看不到任何ORM如何自动地神奇地知道如何进行这些操作。 。
Dusan

2
更令人失望的是,像您这样的问题经常被问到,但是据我所知-这本书中汇总和存储库实现的示例
为零-Dusan

Answers:


5

您的基本理解是正确的,并且您所勾勒出的体系结构很好并且运作良好。

逐行阅读,似乎您来自以数据库为中心的活动记录编程风格?要实现可行的实施,我会说您需要

1:域对象不必包含整个对象图。例如,我可能有:

public class Customer
{
    public string AddressId {get;set;}
    public string Name {get;set;}
}

public class Address
{
    public string Id {get;set;}
    public string HouseNumber {get;set;
}

如果您有一些逻辑,例如“客户名称只能以与房屋名称相同的字母开头”,则“地址”和“客户”仅需要属于同一集合。您应该避免延迟加载和对象的“精简版”版本。

2:唯一性约束通常是存储库的权限,而不是域对象。不要将存储库注入到域对象中,这会回到活动记录,只是在服务尝试保存时出错。

业务规则不是“不能同时存在具有相同SocialSecurityNumber的User的两个实例”

这是因为它们不能存在于同一存储库中。

3:编写存储库而不是编写单个属性更新方法并不难。实际上,您会发现两种方式的代码几乎相同。它是您上哪一堂课的。

这些天的ORM很容易,并且对您的代码没有任何额外的约束。话虽如此,我个人更喜欢简单地手动启动SQL。这并不难,您永远不会遇到ORM功能的任何问题,并且可以在需要的地方进行优化。

确实没有必要跟踪保存时更改的属性。保持您的域对象较小,并简单地覆盖旧版本。

一般的问题

  1. 事务逻辑进入存储库。但是您应该没有太多。如果有要在其中放置聚合子对象的子表,肯定会需要一些子表,但是这些子表将完全封装在SaveMyObject存储库方法中。

  2. 批量更新。是的,您应该单独更改每个对象,然后仅将SaveMyObjects(List objects)方法添加到存储库中以进行批量更新。

    您希望域对象或域服务包含逻辑。不是数据库。这意味着您不能只执行“更新客户集名称= x,其中y”,因为对于您而言,您都知道Customer对象,或者在更改名称时CustomerUpdateService还会执行其他20种事情。


好答案。您是完全正确的,我习惯于采用主动记录风格的编码,这就是为什么存储库模式乍一看似乎很奇怪的原因。但是,“精益”领域对象(AddressId而不是Address)是否与OO原则相矛盾?
Double M

不,您仍然有一个Address对象,它不是Customer的子对象
Ewan


2

简短的回答:您的理解是正确的,并且您提出的问题指向有效的问题,而这些问题的解决方案不是直截了当的,也不是普遍接受的。

点2:(加载完整的对象图)

我不是第一个指出ORM并非总是好的解决方案的人。主要问题是ORM对实际用例一无所知,因此他们不知道要加载什么或如何进行优化。这是个问题。

如您所说,显而易见的解决方案是为每个用例提供持久性方法。但是,如果您仍然为此使用ORM,则ORM将强制您将所有内容打包到数据对象中。除了并非真正面向对象之外,对于某些用例而言,它也不是最佳设计。

如果我只想批量更新一些记录怎么办?为什么我需要所有记录的对象表示形式?等等。

因此,解决方案就是不对不合适的用例使用ORM。照原样实现一个用例,有时不需要对数据本身(数据对象)进行额外的“抽象”,也不需要对“表”(存储库)进行抽象。

正如您所指出的那样,拥有一半填充的数据对象或将对象引用替换为“ id”是最好的解决方法,而不是好的设计。

要点3:(检查约束)

如果没有提取持久性,则每个用例显然可以轻松地检查其想要的任何约束。对象不知道“存储库”的要求完全是人为的,而不是技术问题。

第5点:(ORM)

当然,将具体的存储实现与域代码脱钩是必不可少的前提。但是,确实付出了这么高的代价吗?

不,不是。有很多其他方法可以保持持久性。问题在于,ORM始终(至少对于关系数据库)被视为要使用的“解决方案”。尝试建议不要在项目中的某些用例中使用它是徒劳的,并且有时甚至无法依靠ORM本身,因为这些工具有时会使用高速缓存和后期执行。

一般问题1.:(交易)

我认为没有单一的解决方案。如果您的设计是面向对象的,则每个用例都有一个“顶部”方法。交易应该在那里。

任何其他限制都是完全人为的。

一般问题2.:(批量操作)

使用ORM,您(对于我所知道的大多数ORM)都必须经过单个对象。如果您的手不会被ORM束缚,那么这完全是不必要的,并且可能不是您的设计。

将“逻辑”与SQL分开的要求来自ORM。他们不得不说,因为他们不能支持它。它不是天生的“坏”。

摘要

我想我的观点是,ORM并非始终是项目的最佳工具,即使是,它也不太可能是项目中所有用例的最佳工具。

同样,DDD的数据对象存储库抽象也不总是最好的。我什至可以说,它们很少是最佳设计。

这使得我们没有一个万能的解决方案,因此我们将不得不单独考虑每个用例的解决方案,这不是一个好消息,显然使我们的工作更加困难:)


那里非常有趣的观点,感谢您确认我的假设。您说过还有很多其他方法可以保持持久性。您是否可以推荐可与图形数据库一起使用的高性能设计模式(无ORM),而该数据库仍会提供PI?
Double M

1
我实际上会质疑您是否首先需要隔离(以及哪种隔离)。隔离每种技术(即数据库,用户界面等)几乎可以自动带来您要避免的“尴尬”,其优点是可以更轻松地替换数据库技术。但是,成本是业务逻辑的更困难的更改,因为它会遍及各个层。或者,您可以按业务功能拆分,这将使更改数据库变得更加困难,但使逻辑更改更容易。您到底要哪一个?
罗伯特·布劳蒂加姆(RobertBräutigam)'18年

1
如果仅对域建模(即业务功能),而不对数据库进行抽象化(关系还是图无关紧要),则可以获得最佳性能。由于数据库不是从用例中抽象出来的,因此用例可以实现所需的最佳查询/更新,并且不需要通过笨拙的对象模型来完成所需的操作。
罗伯特·布劳蒂加姆(RobertBräutigam)'18年

好吧,主要目标是使持久性问题脱离业务逻辑,以便获得易于理解,扩展和测试的干净代码。能够交换数据库技术只是一种奖励。我可以看到效率和无知之间显然存在摩擦,由于您可以(但不允许使用)强大的查询,因此对于图数据库而言,这种摩擦似乎更强。
Double M

1
作为Java Enterprise开发人员,我可以告诉您,在过去的二十年中,我们一直在尝试将持久性与逻辑分离。没用 首先,分离从未真正实现。即使在今天,所谓的“业务”对象中也存在各种与数据库有关的东西,主要的是数据库ID(以及许多数据库注释)。第二,正如您所说,有时业务逻辑在数据库中以任何一种方式执行。第三,这就是我们拥有特定数据库的原因,以能够最好地卸载数据所在的某些逻辑。
罗伯特·布劳蒂加姆(RobertBräutigam)'18年
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.