在领域驱动设计中,似乎有很多 < href="https://stackoverflow.com/questions/827670/is-it-ok-for-entities-to-access-repositories/827693#827693">协议实体不应直接访问存储库。
这是来自 Eric Evans 领域驱动设计
对于其背后的推理,哪里有一些好的解释?
编辑:澄清一下:我不是在谈论将数据访问与业务逻辑分离到单独层的经典 OO 实践 - 我是在谈论 DDD 中的具体安排,实体不应该与数据对话完全访问层(即它们不应该保存对存储库对象的引用)
更新:我给了 BacceSR 赏金,因为他的答案似乎最接近,但我对此仍然一无所知。如果这是一个如此重要的原则,那么网上应该有一些关于它的好文章,当然吗?
更新:2013 年 3 月,对该问题的投票表明人们对此很感兴趣,尽管已经有很多答案,但我仍然认为如果人们对此有想法,还有更多的空间。
In Domain Driven Design, there seems to be lots of agreement that Entities should not access Repositories directly.
Did this come from Eric Evans Domain Driven Design book, or did it come from elsewhere?
Where are there some good explanations for the reasoning behind it?
edit: To clarify: I'm not talking about the classic OO practice of separating data access off into a separate layer from the business logic - I'm talking about the specific arrangement whereby in DDD, Entities are not supposed to talk to the data access layer at all (i.e. they are not supposed to hold references to Repository objects)
update: I gave the bounty to BacceSR because his answer seemed closest, but I'm still pretty in the dark about this. If its such an important principle, there should be some good articles about it online somewhere, surely?
update: March 2013, the upvotes on the question imply there's a lot of interest in this, and even though there's been lots of answers, I still think there's room for more if people have ideas about this.
发布评论
评论(13)
这里有点混乱。存储库访问聚合根。聚合根是实体。这样做的原因是关注点分离和良好的分层。这对于小型项目来说没有意义,但如果您在一个大型团队中,您想说,“您通过产品存储库访问产品。产品是实体集合(包括 ProductCatalog 对象)的聚合根。如果您想更新 ProductCatalog,则必须通过 ProductRepository。”
通过这种方式,您可以非常非常清晰地分离业务逻辑和更新内容。你不会有一个孩子独自一人编写整个程序,对产品目录执行所有这些复杂的事情,当涉及到将其集成到上游项目时,你坐在那里看着它并意识到它一切都必须被抛弃。这也意味着当人们加入团队、添加新功能时,他们知道该去哪里以及如何构建程序。
但是等等!存储库也指持久层,如存储库模式中所示。在一个更好的世界中,Eric Evans 的存储库和存储库模式应该有不同的名称,因为它们往往有很多重叠。为了获得存储库模式,您需要与其他访问数据的方式(使用服务总线或事件模型系统)进行对比。通常,当您达到这个级别时,Eric Evans 的存储库定义就会被忽略,您会开始讨论有界上下文。每个有界上下文本质上都是它自己的应用程序。您可能有一个复杂的审批系统来将东西放入产品目录中。在最初的设计中,产品是中心部分,但在这个有限的上下文中,产品目录才是中心部分。您仍然可以通过服务总线访问产品信息并更新产品,但您必须意识到有界上下文之外的产品目录可能意味着完全不同的东西。
回到你原来的问题。如果您从实体内部访问存储库,则意味着该实体实际上不是业务实体,而可能应该存在于服务层中。这是因为实体是业务对象,并且应该关注尽可能类似于 DSL(领域特定语言)。这一层只有业务信息。如果您正在解决性能问题,您就会知道要寻找其他地方,因为这里应该只包含业务信息。如果突然出现应用程序问题,那么扩展和维护应用程序就会变得非常困难,而这正是 DDD 的核心:制作可维护的软件。
对评论 1 的回复:好的,好问题。因此,并非所有验证都发生在域层中。 Sharp 有一个属性“DomainSignature”可以满足您的需求。它具有持久性意识,但作为一个属性可以保持域层的干净。它确保您没有在示例中具有相同名称的重复实体。
但我们来谈谈更复杂的验证规则。假设您是 Amazon.com。您是否曾经使用过期的信用卡订购过商品?我有,但我还没有更新卡并购买东西。它接受订单,用户界面告诉我一切都很顺利。大约 15 分钟后,我会收到一封电子邮件,说我的订单有问题,我的信用卡无效。理想情况下,这里发生的情况是在域层中有一些正则表达式验证。这是正确的信用卡号码吗?如果是,则保留该顺序。但是,在应用程序任务层有额外的验证,其中查询外部服务以查看是否可以通过信用卡进行付款。如果没有,就不要实际发货,暂停订单并等待客户。这一切都应该发生在服务层中。
不要害怕在可以访问存储库的服务层创建验证对象。只要将其置于域层之外即可。
There's a bit of a confusion here. Repositories access aggregate roots. Aggregate roots are entities. The reason for this is separation of concerns and good layering. This doesn't make sense on small projects, but if you're on a large team you want to say, "You access a product through the Product Repository. Product is an aggregate root for a collection of entities, including the ProductCatalog object. If you want to update the ProductCatalog you must go through the ProductRepository."
In this way you have very, very clear separation on the business logic and where things get updated. You don't have some kid who is off by himself and writes this entire program that does all these complicated things to the product catalog and when it comes to integrate it to the upstream project, you're sitting there looking at it and realize it all has to be ditched. It also means when people join the team, add new features, they know where to go and how to structure the program.
But wait! Repository also refers to the persistence layer, as in the Repository Pattern. In a better world an Eric Evans' Repository and the Repository Pattern would have separate names, because they tend to overlap quite a bit. To get the repository pattern you have contrast with other ways in which data is accessed, with a service bus or an event model system. Usually when you get to this level, the Eric Evans' Repository definition goes by the way side and you start talking about a bounded context. Each bounded context is essentially its own application. You might have a sophisticated approval system for getting things into the product catalog. In your original design the product was the center piece but in this bounded context the product catalog is. You still might access product information and update product via a service bus, but you must realize that a product catalog outside the bounded context might mean something completely different.
Back to your original question. If you're accessing a repository from within an entity it means the entity is really not a business entity but probably something that should exist in a service layer. This is because entities are business object and should concern themselves with being as much like a DSL (domain specific language) as possible. Only have business information in this layer. If you're troubleshooting a performance issue, you'll know to look elsewhere since only business information should be here. If suddenly, you have application issues here, you're making it very hard to extend and maintain an application, which is really the heart of DDD: making maintainable software.
Response to Comment 1: Right, good question. So not all validation occurs in the domain layer. Sharp has an attribute "DomainSignature" that does what you want. It is persistence aware, but being an attribute keeps the domain layer clean. It ensures that you don't have a duplicate entity with, in your example the same name.
But let's talk about more complicated validation rules. Let's say you're Amazon.com. Have you ever ordered something with an expired credit card? I have, where I haven't updated the card and bought something. It accepts the order and the UI informs me that everything is peachy. About 15 minutes later, I'll get an e-mail saying there's a problem with my order, my credit card is invalid. What's happening here is that, ideally, there's some regex validation in the domain layer. Is this a correct credit card number? If yes, persist the order. However, there's additional validation at the application tasks layer, where an external service is queried to see if payment can be made on the credit card. If not, don't actually ship anything, suspend the order and wait for the customer. This should all take place in a service layer.
Don't be afraid to create validation objects at the service layer that can access repositories. Just keep it out of the domain layer.
起初,我劝说允许我的一些实体访问存储库(即没有 ORM 的延迟加载)。后来我得出的结论是我不应该这样做,我可以找到替代方法:
Vernon Vaughn 在红皮书 Implementing Domain-Driven Design 中据我所知在两个地方提到了这个问题(注:这本书得到了 Evans 的完全认可,你可以在前言中读到)。在有关服务的第 7 章中,他使用域服务和规范来解决聚合使用存储库和另一个聚合来确定用户是否经过身份验证的需求。引用他的话说:
弗农,沃恩 (2013-02-06)。实施领域驱动设计(Kindle 位置 6089)。培生教育。 Kindle 版。
以及关于聚合的第 10 章中的 部分他在题为“模型导航” 的文章中说道(就在他建议使用全局唯一 ID 来引用其他聚合根之后):
他接着在代码中展示了一个示例:
他接着还提到了如何在聚合命令方法中使用域服务以及 的另一种解决方案双重调度。 (我无法充分推荐阅读他的书有多么有益。当你厌倦了无休止地在互联网上翻阅之后,花当之无愧的钱来阅读这本书。)
然后我吃了一些与总是亲切的 Marco Pivetta @Ocramius 进行讨论 他向我展示了一些从域中提取规范并使用它的代码:
At first, I was of the persuasion to allow some of my entities access to repositories (i.e. lazy loading without an ORM). Later I came to the conclusion that I shouldn't and that I could find alternate ways:
Vernon Vaughn in the red book Implementing Domain-Driven Design refers to this issue in two places that I know of (note: this book is fully endorsed by Evans as you can read in the foreword). In Chapter 7 on Services, he uses a domain service and a specification to work around the need for an aggregate to use a repository and another aggregate to determine if a user is authenticated. He's quoted as saying:
Vernon, Vaughn (2013-02-06). Implementing Domain-Driven Design (Kindle Location 6089). Pearson Education. Kindle Edition.
And in Chapter 10 on Aggregates, in the section titled "Model Navigation" he says (just after he recommends the use of global unique IDs for referencing other aggregate roots):
He goes onto show an example of this in code:
He goes on to also mention yet another solution of how a domain service can be used in an Aggregate command method along with double-dispatch. (I can't recommend enough how beneficial it is to read his book. After you have tired from endlessly rummaging through the internet, fork over the well deserved money and read the book.)
I then had some discussion with the always gracious Marco Pivetta @Ocramius who showed me a bit of code on pulling out a specification from the domain and using that:
这是一个非常好的问题。我期待对此进行一些讨论。但我认为几本 DDD 书籍以及 Jimmy nilssons 和 Eric Evans。我想通过示例也可以看出如何使用存储库模式。
但我们来讨论一下。我认为一个非常有效的想法是为什么一个实体应该知道如何持久化另一个实体? DDD 的重要之处在于,每个实体都有责任管理自己的“知识领域”,并且不应该知道如何读取或写入其他实体。当然,您可能只需向实体 A 添加一个存储库接口来读取实体 B。但风险在于您暴露了如何持久化 B 的知识。在将 B 持久化到数据库之前,实体 A 是否也会对 B 进行验证?
正如您所看到的,实体 A 可以更多地参与实体 B 的生命周期,这会增加模型的复杂性。
我想(没有任何例子)单元测试会更加复杂。
但我确信在某些情况下您总是想通过实体使用存储库。您必须查看每个场景才能做出有效的判断。优点和缺点。但在我看来,存储库实体解决方案有很多缺点。这一定是一个非常特殊的场景,优点平衡缺点......
Its a very good question. I will look forward to some discussion about this. But I think it's mentioned in several DDD books and Jimmy nilssons and Eric Evans. I guess it's also visible through examples how to use the reposistory pattern.
BUT lets discuss. I think a very valid thought is why should an entity know about how to persist another entity? Important with DDD is that each entity has a responsibility to manage its own "knowledge-sphere" and shouldn't know anything about how to read or write other entities. Sure you can probably just add a repository interface to Entity A for reading Entities B. But the risk is that you expose knowledge for how to persist B. Will entity A also do validation on B before persisting B into db?
As you can see entity A can get more involved into entity B's lifecycle and that can add more complexity to the model.
I guess (without any example) that unit-testing will be more complex.
But I'm sure there will always be scenarios where you're tempted to use repositories via entities. You have to look at each scenario to make a valid judgement. Pros and Cons. But the repository-entity solution in my opinion starts with a lot of Cons. It must be a very special scenario with Pros that balance up the Cons....
多么好的问题啊。我走在同样的发现之路上,互联网上的大多数答案似乎带来的问题和解决方案一样多。
所以(冒着一年后我不同意写一些东西的风险)这是我迄今为止的发现。
首先,我们喜欢丰富的领域模型,它为我们提供了高可发现性(我们可以使用聚合做什么)和可读性(表达方法调用)。
我们希望在不向实体的构造函数中注入任何服务的情况下实现这一目标,因为:
那么,我们怎样才能做到这一点呢?到目前为止我的结论是,方法依赖和双重调度提供了一个不错的解决方案。
CreateCreditNote()
现在需要一个负责创建贷方票据的服务。它使用双重调度,将工作完全卸载给负责的服务,同时从Invoice
实体保持可发现性。SetStatus()
现在对记录器具有简单依赖,显然它将执行部分工作。对于后者,为了使客户端代码更容易,我们可以通过
IInvoiceService
进行日志记录。毕竟,发票记录似乎是发票固有的。这样一个IInvoiceService
有助于避免各种操作需要各种微型服务。缺点是该服务到底要做什么变得模糊。。它甚至可能开始看起来像双重调度,而大部分工作实际上仍然是在SetStatus()
本身中完成的。我们仍然可以将参数命名为“logger”,以期揭示我们的意图。不过好像有点弱。
相反,我会选择请求一个
IInvoiceLogger
(正如我们在代码示例中所做的那样)并让IInvoiceService
实现该接口。客户端代码可以简单地将其单个IInvoiceService
用于所有请求任何此类非常特殊的发票固有“迷你服务”的Invoice
方法,而方法签名仍然充分明确他们的要求。我注意到我没有明确提及存储库。好吧,记录器是或使用存储库,但让我还提供一个更明确的示例。如果仅在一两个方法中需要存储库,我们可以使用相同的方法。
事实上,这为一直麻烦的延迟加载提供了一种替代方案。
更新:出于历史目的,我保留了下面的文本,但我建议 100% 避免延迟加载。
对于真正的基于属性的延迟加载,我当前使用构造函数注入,但是以一种持久性无知的方式。
一方面,从数据库加载
Invoice
的存储库可以自由访问加载相应贷方票据的函数,并将该函数注入到Invoice
中。另一方面,创建实际new
Invoice
的代码将仅传递返回空列表的函数:(自定义
ILazy 可以让我们摆脱对
IEnumerable
的丑陋转换,但这会使讨论变得复杂。)我很高兴听到您的意见、偏好和改进!
What an excellent question. I am on the same path of discovery, and most answers throughout the internet seem to bring as many problems as they bring solutions.
So (at the risk of writing something that I disagree with a year from now) here are my discoveries so far.
First of all, we like a rich domain model, which gives us high discoverability (of what we can do with an aggregate) and readability (expressive method calls).
We want to achieve this without injecting any services into an entity's constructor, because:
How, then, can we do this? My conclusion so far is that method dependencies and double dispatch provide a decent solution.
CreateCreditNote()
now requires a service that is responsible for creating credit notes. It uses double dispatch, fully offloading the work to the responsible service, while maintaining discoverability from theInvoice
entity.SetStatus()
now has a simple dependency on a logger, which obviously will perform part of the work.For the latter, to make things easier on the client code, we might instead log through an
IInvoiceService
. After all, invoice logging seems pretty intrinsic to an invoice. Such a singleIInvoiceService
helps avoid the need for all sorts of mini-services for various operations. The downside is that it becomes obscure what exactly that service will do. It might even start to look like double dispatch, while most of the work is really still done inSetStatus()
itself.We could still name the parameter 'logger', in hopes of revealing our intent. Seems a bit weak, though.
Instead, I would opt to ask for an
IInvoiceLogger
(as we already do in the code sample) and haveIInvoiceService
implement that interface. The client code can simply use its singleIInvoiceService
for allInvoice
methods that ask for any such a very particular, invoice-intrinsic 'mini-service', while the method signatures still make abundantly clear what they are asking for.I notice that I have not addressed repositories exlicitly. Well, the logger is or uses a repository, but let me also provide a more explicit example. We can use the same approach, if the repository is needed in just a method or two.
In fact, this provides an alternative to the ever-troublesome lazy loads.
Update: I have left the text below for historical purposes, but I suggest steering clear of lazy loads 100%.
For true, property-based lazy loads, I do currently use constructor injection, but in a persistence-ignorant way.
On the one hand, a repository that loads an
Invoice
from the database can have free access to a function that will load the corresponding credit notes, and inject that function into theInvoice
.On the other hand, code that creates an actual new
Invoice
will merely pass a function that returns an empty list:(A custom
ILazy<out T>
could rid us of the ugly cast toIEnumerable
, but that would complicate the discussion.)I'd be happy to hear your opinions, preferences, and improvements!
为什么要分离数据访问?
从书中,我认为模型驱动设计一章的前两页给出了为什么要从领域模型的实现中抽象出技术实现细节的一些理由。
这似乎都是为了避免独立的“分析模型”与系统的实际实现脱节。
根据我对这本书的理解,它说这个“分析模型”最终可以在不考虑软件实现的情况下设计。一旦开发人员尝试实现业务方理解的模型,他们就会出于必要而形成自己的抽象,从而造成沟通和理解的障碍。
另一方面,开发人员在领域模型中引入过多的技术问题也可能导致这种分歧。
因此,您可以考虑实践关注点分离(例如持久性)可以帮助防止这些设计和分析模型出现分歧。如果感觉有必要将持久性之类的东西引入模型中,那么这是一个危险信号。也许该模型不适合实施。
引用:
“单一模型减少了出错的机会,因为设计现在是经过仔细考虑的模型的直接产物。设计,甚至代码本身,都具有模型的交流性。”
按照我的解释,如果您最终使用更多行代码来处理数据库访问之类的事情,那么您就会失去这种沟通能力。
如果访问数据库的目的是检查唯一性之类的事情,请查看:
Udi Dahan:团队在应用 DDD 时犯的最大错误
http://gojko.net/2010/06/11/udi-dahan-the-biggest-mistakes-teams-make-when-applying-ddd/
下的“所有规则均是” t 创建相等”
和
采用域模型模式
http://msdn.microsoft.com/en-us/magazine/ee236415.aspx#id0400119
下的“不使用域模型的场景”,涉及同一主题。
如何分离数据访问
通过接口加载数据
“数据访问层”已通过接口进行抽象,您可以调用该接口来检索所需的数据:
优点:该接口分离出“数据访问”管道代码,使您能够仍然编写测试。数据访问可以根据具体情况进行处理,从而获得比通用策略更好的性能。
缺点:调用代码必须假设哪些内容已加载,哪些内容尚未加载。
假设出于性能原因,GetOrderLines 返回具有 null ProductInfo 属性的 OrderLine 对象。开发人员必须对界面背后的代码有深入的了解。
我已经在真实系统上尝试过这种方法。为了解决性能问题,您最终会不断更改加载内容的范围。您最终会在界面后面查看数据访问代码,以了解正在加载和未加载的内容。
现在,关注点分离应该允许开发人员尽可能地同时关注代码的一个方面。接口技术删除了该数据如何加载,但不删除加载多少数据、加载时间和加载位置。
结论:分离度相当低!
延迟加载
数据按需加载。对加载数据的调用隐藏在对象图本身中,其中访问属性可能会导致在返回结果之前执行 SQL 查询。
优点:数据访问的“时间、地点和方式”对于专注于域逻辑的开发人员来说是隐藏的。聚合中没有处理加载数据的代码。加载的数据量可以是代码所需的确切数量。
缺点:当您遇到性能问题时,如果您有通用的“一刀切”解决方案,则很难修复。延迟加载可能会导致整体性能变差,并且实现延迟加载可能很棘手。
角色接口/急切获取
每个用例都通过聚合类实现的 角色接口 明确化,允许根据用例处理数据加载策略。
获取策略可能如下所示:
然后您的聚合可能如下所示:
BillOrderFetchingStrategy 用于构建聚合,然后聚合执行其工作。
优点:允许每个用例使用自定义代码,从而实现最佳性能。符合接口隔离原则。没有复杂的代码要求。聚合单元测试不必模仿加载策略。通用加载策略可以用于大多数情况(例如“加载全部”策略),并且可以在必要时实施特殊加载策略。
缺点:更改域代码后,开发人员仍然需要调整/审查获取策略。
使用获取策略方法,您可能仍然会发现自己正在更改自定义获取代码以适应业务规则的变化。这不是一个完美的关注点分离,但最终会更易于维护,并且比第一个选项更好。获取策略确实封装了数据加载的方式、时间和地点。它具有更好的关注点分离,而不会失去像一刀切的延迟加载方法那样的灵活性。
Why separate out data access?
From the book, I think the first two pages of the chapter Model Driven Design gives some justification for why you want to abstract out technical implementation details from the implementation of the domain model.
This seems to be all for the purpose of avoiding a separate "analysis model" that becomes divorced from the actual implementation of the system.
From what I understand of the book, it says this "analysis model" can end up being designed without considering software implementation. Once developers try to implement the model understood by the business side they form their own abstractions due to necessity, causing a wall in communication and understanding.
In the other direction, developers introducing too many technical concerns into the domain model can cause this divide as well.
So you could consider that practicing separation of concerns such as persistence can help safeguard against these design an analysis models diverging. If it feels necessary to introduce things like persistence into the model then it is a red flag. Maybe the model is not practical for implementation.
Quoting:
"The single model reduces the chances of error, because the design is now a direct outgrowth of the carefully considered model. The design, and even the code itself, has the communicativeness of a model."
The way I'm interpreting this, if you ended up with more lines of code dealing with things like database access, you lose that communicativeness.
If the need for accessing a database is for things like checking uniqueness, have a look at:
Udi Dahan: the biggest mistakes teams make when applying DDD
http://gojko.net/2010/06/11/udi-dahan-the-biggest-mistakes-teams-make-when-applying-ddd/
under "All rules aren't created equal"
and
Employing the Domain Model Pattern
http://msdn.microsoft.com/en-us/magazine/ee236415.aspx#id0400119
under "Scenarios for Not Using the Domain Model", which touches on the same subject.
How to separate out data access
Loading data through an interface
The "data access layer" has been abstracted through an interface, which you call in order to retrieve required data:
Pros: The interface separates out the "data access" plumbing code, allowing you to still write tests. Data access can be handled on a case by case basis allowing better performance than a generic strategy.
Cons: The calling code must assume what has been loaded and what hasn't.
Say GetOrderLines returns OrderLine objects with a null ProductInfo property for performance reasons. The developer must have intimate knowledge of the code behind the interface.
I've tried this method on real systems. You end up changing the scope of what is loaded all the time in an attempt to fix performance problems. You end up peeking behind the interface to look at the data access code to see what is and isn't being loaded.
Now, separation of concerns should allow the developer to focus on one aspect of the code at one time, as much as is possible. The interface technique removes the HOW is this data loaded, but not HOW MUCH data is loaded, WHEN it is loaded, and WHERE it is loaded.
Conclusion: Fairly low separation!
Lazy Loading
Data is loaded on demand. Calls to load data is hidden within the object graph itself, where accessing a property can cause a sql query to execute before returning the result.
Pros: The 'WHEN, WHERE, and HOW' of data access is hidden from the developer focusing on domain logic. There is no code in the aggregate that deals with loading data. The amount of data loaded can be the exact amount required by the code.
Cons: When you are hit with a performance problem, it is hard to fix when you have a generic "one size fits all" solution. Lazy loading can cause worse performance overall, and implementing lazy loading may be tricky.
Role Interface/Eager Fetching
Each use case is made explicit via a Role Interface implemented by the aggregate class, allowing for data loading strategies to be handled per use case.
Fetching strategy may look like this:
Then your aggregate can look like:
The BillOrderFetchingStrategy is use to build the aggregate, and then the aggregate does its work.
Pros: Allows for custom code per use case, allowing for optimal performance. Is inline with the Interface Segregation Principle. No complex code requirements. Aggregates unit tests do not have to mimic loading strategy. Generic loading strategy can be used for majority of cases (e.g. a "load all" strategy) and special loading strategies can be implemented when necessary.
Cons: Developer still has to adjust/review fetching strategy after changing domain code.
With the fetching strategy approach you might still find yourself changing custom fetching code for a change in business rules. It's not a perfect separation of concerns but will end up more maintainable and is better than the first option. The fetching strategy does encapsulate the HOW, WHEN and WHERE data is loaded. It has a better separation of concerns, without losing flexibility like the one size fits all lazy loading approach.
对我来说,这似乎是与 OOD 相关的一般良好实践,而不是特定于 DDD。
我能想到的原因是:
To me this appears to be general good OOD related practice rather than being specific to DDD.
Reasons that I can think of are:
实体仅捕获与其有效状态相关的规则。其中的数据有效吗?他们里面的数据能这样改变吗?
聚合根对一组实体执行相同的操作。汇总的数据有效吗?聚合中的数据能这样改变吗?
领域服务捕获有关跨实体或聚合的更改的规则。我们可以这样改变X和Y吗?
所有这些都不需要访问存储库或基础设施。您所做的是应用程序服务将提供一个域用例,对于该用例,应用程序服务将从存储库收集所有所需的数据,这将返回您的域实体和/或聚合根及其值对象。实体/聚合根和值对象将验证它们在由存储库创建时处于良好状态。然后,应用程序服务将使用这些实体的组合(其中一些可能是聚合根)来执行域用例。如果域用例需要更改 X、Y 和 Z,则应用程序服务将询问 X、Y 和 Z 实体/聚合根当前用例请求是否可以对 X、Y 和 Z 进行更改,如果可以,如何进行是否应该制作。最后,应用程序服务会将这些更改提交回存储库。
如果某些更改跨越实体或聚合,应用程序服务将使用域服务来询问是否可以进行更改以及如果可以如何进行更改,然后再次使用存储库来提交这些更改。
如果一个域用例跨越多个有界上下文,这意味着它需要跨有界上下文的信息或更改,这称为流程,您可以让流程服务管理整个流程生命周期,它将利用多个有界上下文来协调所有有界上下文的完整过程。
最后,应用程序服务还可以使用其他应用程序服务,可以是共享有界上下文中的其他微服务,这意味着它们共享相同的域模型,或者它可以跨其他有界上下文中的应用程序服务这样做,其中如果您也想在自己的有界上下文的域模型中对它们进行建模,那么您会在某种程度上将其他有界上下文视为存储库。应用程序服务与另一个有界上下文通信以获取有关该其他上下文的信息,然后它使用自己的实体和 VO 以及聚合在自己的域模型中创建该信息的表示,这将再次在其上下文中验证该状态。同样,您可以通过要求其他有界上下文进行相应更改来将对域模型的更改提交给其他有界上下文。所有这些都可以通过直接方法调用、远程 API 调用、异步事件、共享内核等来实现。
要回答为什么会这样,那是因为重点是构建可以随着时间的推移而发展的软件,而不会变得更慢对其进行更改并添加/修改其行为,同时保留其当前功能的正确性。做到这一点的一个好方法是在一个地方进行更改,而不会破坏其他地方的东西。这就是有界上下文存在的原因,更改已经限制在每个上下文中,因此一个上下文中的更改不太可能破坏另一个上下文。这也是域模型验证域状态的所有更改的原因,因此您不能以破坏状态的其他用法的方式更改部分状态。这就是为什么使用聚合,以在需要聚合的事物之间维持变化边界,而在不需要聚合的事物之间显然没有变化边界。最后,通过拥有具有域模型和域服务的整个域层,不依赖于任何基础设施,例如存储库(以及数据库),对数据库或存储库的更改也将无法破坏您的域模型或服务。
PS:另请注意,我宽松地使用了“状态”一词。它不必是静态值;状态可以是一些动态计算或规则的应用,在请求时生成状态。您可以在某个实体上拥有类似 TotalItemsCount 的内容,当询问该实体当前的 TotalItemsCount 是多少时,它会计算它。同样,实体将确保返回有效状态,这意味着它将知道如何正确计算总数,并确保返回的是totalItemsCount 域规则的正确应用。
Entities only capture the rules related to their valid state. Is the data in them valid? Can the data in them change in this way?
An aggregate root does the same for a group of entities. Is the data in the aggregate valid? Can the data in the aggregate change in this way?
Domain services capture rules about changes across entities or aggregates. Can we change X and Y this way?
None of this ever requires access to a repository or to infrastructure. What you do is that an application service will offer up a domain use case, for that use case, the application service will gather all the needed data from the repositories, that will return it your domain entities and/or aggregate roots and their value objects. The entities/aggregate roots and value objects would have validated that they are in a good state when created by the repository. Then the application service will use a combination of those entities (some of them could be aggregate roots), to perform the domain use case. If the domain use case requires changing X, Y and Z, the application service will ask X, Y and Z entities/aggregate roots if the current use case request of changes can be made to X, Y and Z, and if so, how should it be made. Finally, the application service will commit those changes back to the repository.
If some change spans across entities or aggregates, the application service will use a domain service to ask if the change can be made and if so how, and once again will use the repository to commit those changes.
If a domain use case spans multiple bounded contexts, that means it requires information or changes across bounded contexts, this is called a process, and you can have a process service manage the full process life-cycle, it will make use of application services of multiple bounded contexts to coordinate the full process across all bounded contexts.
Finally, the application service can also use other application services, could be other micro-services in a shared bounded context, that would imply they share the same domain model, or it could do so across to application services in other bounded contexts, in which case you'd want to model those within your own bounded context's domain model as well, you'd treat those other bounded contexts much like a repository in a way. The application service communicates with another bounded context to get info about that other context, it then creates a representation of that info within its own domain model, using its own entities and VOs, and aggregates, which will again validate that state within their context. Similarly, you can commit changes to your domain model to other bounded contexts by asking them to change accordingly. All this can be implemented with direct method calls, remote API calls, async events, shared kernel, etc.
And to answer why it is like so, that's because the whole point is building software that can evolve over time without it becoming slower to make changes to it and add/modify its behavior while retaining its current correctness with regards to its current functionality. A good way to do this is by making it a change in one place doesn't break things elsewhere. This is why bounded contexts exist, already changes are restricted to each context, so a change in one is less likely to break another. This is also why the domain model validates all changes to the domain state, so you can't change part of the state in ways that breaks other usage of it. This is why aggregates are used, to maintain a change boundary between the things that need one, and clearly not have one where it doesn't need one. Finally, by having the whole domain layer, with domain model and domain services, not depend on any infrastructure, like the repository (and thus the DB), a change to the DB or repository will also not be able to break your domain model or services.
P.S.: Also note I use the term "state" loosely. It doesn't have to be a static value; state could be the application of some dynamic computation or rules that generates state when requested. You can have something like totalItemsCount on some entity which computes it when asked about what is the current totalItemsCount for the entity. Again, the entity will make sure to return you valid state, that means it will know how to correctly count the total and make sure that what is returned is the correct application of the domain rules for totalItemsCount.
都是老东西了埃里克的书让这件事变得更加热闹。
原因很简单——当人类面对模糊相关的多个情境时,人类的思维就会变得脆弱。它们会导致歧义(南/北美洲中的美国意味着南/北美洲),每当思想“触及”它时,歧义就会导致不断的信息映射,这总结为低生产力和错误。
业务逻辑应该尽可能清晰地反映。外键、规范化、对象关系映射来自完全不同的领域——这些东西与技术、计算机相关。
打个比方:如果你正在学习如何手写,你不应该被理解笔是在哪里制造的、为什么墨水会留在纸上、纸是何时发明的以及中国其他著名的发明是什么所困扰。
原因仍然与我上面提到的相同。这里只是更进一步。如果实体可以(至少接近)完全无知,为什么它们应该部分持久无知?我们的模型所持有的与领域无关的关注点更少——当我们的大脑必须重新解释它时,它会获得更多的喘息空间。
It's old stuff. Eric`s book just made it buzz a bit more.
Reason is simple - human mind gets weak when it faces vaguely related multiple contexts. They lead to ambiguousness (America in South/North America means South/North America), ambiguousness leads to constant mapping of information whenever mind "touches it" and that sums up as bad productivity and errors.
Business logic should be reflected as clearly as possible. Foreign keys, normalization, object relational mapping are from completely different domain - those things are technical, computer related.
In analogy: if you are learning how to handwrite, you shouldn't be burdened with understanding where pen was made, why ink holds on paper, when paper was invented and what are other famous Chinese inventions.
Reason is still the same I mentioned above. Here it's just one step further. Why entities should be partially persistence ignorant if they can be (at least close to) totally? Less domain-unrelated concerns our model holds - more breathing room our mind gets when it has to re-interpret it.
Vernon Vaughn 简单地给出了一个解决方案:
simply Vernon Vaughn gives a solution:
在所有这些单独的层嗡嗡声出现之前,我学会了编写面向对象编程,并且我的第一个对象/类确实直接映射到数据库。
最终,我添加了一个中间层,因为我必须迁移到另一个数据库服务器。我多次看到/听说过同样的场景。
我认为将数据访问(又名“存储库”)与业务逻辑分离是其中之一,尽管《领域驱动设计》一书已经多次重新发明,但它却带来了很多“噪音”。
我目前使用 3 层(GUI、逻辑、数据访问),就像许多开发人员所做的那样,因为它是一项很好的技术。
将数据分离到存储库层(又名数据访问层)可能被视为一种良好的编程技术,而不仅仅是需要遵循的规则。
与许多方法一样,您可能希望从不实施开始,并最终在理解它们后更新您的程序。
引用:
《伊利亚特》并不完全是荷马发明的,《布兰诗歌》也不完全是卡尔·奥尔夫发明的,在这两种情况下,把其他人的工作放在一起的人得到了荣誉;-)
I learnt to code object oriented programming before all this separate layer buzz appear, and my first objects / classes DID map directly to the database.
Eventually, I added an intermediate layer because I had to migrate to another database server. I have seen / heard about the same scenario several times.
I think separating the data access (a.k.a. "Repository") from your business logic, is one of those things, that have been reinvented several times, altought the Domain Driven Design book, make it a lot of "noise".
I currently use 3 layers (GUI, Logic, Data Access), like many developer does, because its a good technique.
Separating the data, into a
Repository
layer (a.k.a.Data Access
layer), may be seen like a good programming technique, not just a rule, to follow.Like many methodologies, you may want to start, by NOT implemented, and eventually, update your program, once you understand them.
Quote:
The Iliad wasn't totally invented by Homer, Carmina Burana wasn't totally invented by Carl Orff, and in both cases, the person who put others work, all togheter, got the credit ;-)
引用 Carolina Lilientahl 的话,“模式应该防止循环”https://www.youtube.com/watch ?v=eJjadzMRQAk,她指的是类之间的循环依赖关系。对于聚合内部的存储库,出于对象导航的便利性而存在创建循环依赖关系的诱惑,这是唯一的原因。 Prograhammer 上面提到的模式是 Vernon Vaughn 推荐的,其中其他聚合由 ids 而不是根实例引用(该模式有名称吗?)建议了一种可能引导其他解决方案的替代方案。
类之间循环依赖的示例(坦白):
(Time0):两个类 Sample 和 Well 相互引用(循环依赖)。 Well 指的是 Sample,Sample 指的是 Well,为了方便起见(有时循环样品,有时循环板中的所有孔)。我无法想象样本不会引用它所在的井的情况。
(时间 1):一年后,实施了许多用例......并且现在存在样品不应引用回其所在孔的情况。工作步骤中存在临时板。这里的孔指的是样品,而样品又指另一个板上的孔。因此,当有人尝试实现新功能时,有时会出现奇怪的行为。需要时间去渗透。
我也得到了这篇文章提到的帮助上面关于延迟加载的负面影响。
To cite Carolina Lilientahl, "Patterns should prevent cycles" https://www.youtube.com/watch?v=eJjadzMRQAk, where she refers to cyclic dependencies between classes. In case of repositories inside aggregates, there is a temptation to create cyclic dependencies out of conveniance of object navigation as the only reason. The pattern mentioned above by prograhammer, that was recommended by Vernon Vaughn, where other aggregates are referenced by ids instead of root instances, (is there a name for this pattern?) suggests an alternative that might guide into other solutions.
Example of cyclic dependency between classes (confession):
(Time0): Two classes, Sample and Well, refer to each other (cyclic dependency). Well refers to Sample, and Sample refers back to Well, out of convenience (sometimes looping samples, sometimes looping all wells in a plate). I couldn't imagine cases where Sample would not reference back to the Well where it's placed.
(Time1): A year later, many use cases are implemented .... and there are now cases where Sample should not reference back to the Well it's placed in. There are temporary plates within a working step. Here a well refers to a sample, which in turn refers to a well on another plate. Because of this, weird behaviour sometimes occurs when somebody tries to implement new features. Takes time to penetrate.
I also was helped by this article mentioned above about negative aspects of lazy loading.
参加聚会已经很晚了,但我会给我 2 美分。
从性能的角度来看,在域模型中抽象 REST API 操作的存储库和域服务可能是一场重大灾难。我认为域服务(尽管红皮书中另有说明!)和聚合都不应该尝试使用它们,并且这两个概念应该只保留在应用程序服务领域,它唯一的责任是无论您使用 Layers 还是 Hexagon(端口和适配器),都可以与外界通信。
通过这种方式,所有昂贵的 I/O 通信都由一个应用程序服务进行分配和完全控制。它将:
构建正确的对象图,在应用程序服务中使用正确的获取策略,并将纯内存中的对象传递给富域模型。延迟加载会潜入您的代码并给您带来最严重的伤害。
Very late to the party but I'll give my 2 cents.
Repositories and Domain services abstracting REST API operations in the domain model can be a major disaster from performance standpoint. I would argue that nor domain service (despite said otherwise in Red Book!), nor aggregate should try working with them and that those two concepts should remain only in realm of Application Service which has a sole responsibility to communicate with outside world, no matter you use Layers or Hexagon (Ports & Adapters).
In this way all expensive I/O communication is allocated and completely controlled by one Application Service. It will:
Build proper object graph, use correct fetching strategies in the application service and just pass pure in-memory objects to rich domain model. Lazy loading will sneak into your code and hit you where it hurts most.
在理想世界中,DDD 提出实体不应该引用数据层。但我们并不生活在理想的世界中。域可能需要引用其他域对象来实现业务逻辑,而它们可能不具有依赖关系。实体出于只读目的引用存储库层以获取值是合乎逻辑的。
In the ideal world , DDD proposes that Entities should not have reference to data layers. but we do not live in ideal world. Domains may need to refer to other domain objects for business logic with whom they might not have a dependency. It is logical for entities to refer to repository layer for read only purpose, to fetch the values.