领域驱动设计和 IoC/依赖注入
Posted
技术标签:
【中文标题】领域驱动设计和 IoC/依赖注入【英文标题】:Domain Driven Design and IoC/Dependency Injection 【发布时间】:2012-12-10 19:23:27 【问题描述】:我现在正在尝试应用我学到的关于 DDD 的知识,但我对域模型中的依赖关系流有点困惑。
我的问题是:
-
实体是否应了解域中的工厂、存储库、服务?
存储库是否应了解域中的服务?
另一个困扰我的事情是当我想向集合添加实体时如何处理集合。
假设我正在开发一个简单的 CMS。在 CMS 中,我有一个包含标签实体的文章实体和标签集合。
现在,如果我想添加一个带有新标签的关系。更好的方法是什么? (php 中的示例)
$article->tags->add(TagEntity);
$articleRepository->save($article);
或者我可以通过服务来做到这一点。
$articleService->addTag($article, TagEntity);
你怎么看?
谢谢。
【问题讨论】:
如果您正在开发“简单的 CMS”,DDD 可能不是最好的模式。根据 Evens 的说法,当领域很复杂时,DDD 是合理的。 谢谢。我不是在开发 CMS。我只是举了这个例子,因为不用深入我的领域就可以理解。 参见:***.com/questions/13527621/…我也有类似的问题 【参考方案1】:我会在我认为没有正确答案的前提下回答这个问题,只是方法不同。
从域对象的角度考虑,我会说第一种方法是 DDD。您纯粹是在处理域对象。
我确实认为,服务对象有一些用途,将其作为 API/服务层的一部分公开。在这种情况下,您可以将#1 中的代码包装在您的服务#2 中。这使您可以避免将域对象暴露给外部消费者 - 并且您可以在更新域模型时保持外部接口/API 不变。
但这只是一种观点。
【讨论】:
谢谢。在这种情况下,您将如何处理从集合中删除对象?您如何看待第一个关于谁应该知道谁的问题?谢谢。【参考方案2】:实体是否应该知道工厂、存储库、服务 域?
实体不应引用存储库或应用程序服务。如果实体使用工厂来创建组成实体,那么实体引用工厂是可以接受的。如果实体将域服务用于某些行为,那么实体对域服务的依赖也是可以接受的。
存储库应该知道域中的服务吗?
一般不会。存储库应该只负责持久性。
现在,如果我想添加一个带有新标签的关系。会是什么 更好的方法?
这取决于你指的是哪一层。在典型的 DDD 架构中,您将拥有两段代码。您将拥有一个文章应用程序服务,它封装了域并提供了一个细粒度的方法,例如addTag
,您将在其中传递一个文章 ID 和一个标签 ID。此方法将检索适当的文章和标签实例(如果需要),然后:
$article->tags->add(TagEntity);
$articleRepository->save($article);
依赖于该域的所有外层都将通过应用程序服务与其通信。
【讨论】:
在这种情况下,您将如何实现聚合内实体的延迟加载。例如,如果我不需要使用标签,我就不应该查询它们。如果实体不知道存储库,您会怎么做?谢谢 在这种情况下实现延迟加载的方式是通过使用反射和代理(例如在 Hibernate 中)。我已经多年没有使用 PHP,我不确定 PHP 是否有任何支持它的 ORM。但是,需要注意的另一件重要事情是 lazy loading can be problematic 并且可以通过使用 read-models 来避免。 @tounano 这就是我不同意 eulerfx 的原因:反射和类似的“技巧”不应该是允许延迟加载的强制性要求。实体可以使用域服务接口(实现可以在任何层)。由于存储库是一项服务,因此实体可以使用存储库接口(位于域层中),而存储库实现位于基础架构层中。有关详细信息,请参阅我的答案。 “一个实体永远不应该引用存储库或应用程序服务”你从哪里得到的?【参考方案3】:实体和值对象不应该相互依赖。这些是所有building blocks of DDD 中最基本的。它们代表了您的问题域的概念,因此应该关注问题。通过使它们依赖于工厂、存储库和服务,您会使焦点变得模糊。在实体和值对象中引用服务还有另一个问题。因为服务也拥有领域逻辑,你会很想将领域模型的一些职责委托给服务,这最终可能导致Anemic Domain Model。
工厂和存储库只是用于创建和持久化实体的助手。大多数时候,它们只是在实际问题域中没有类比,因此根据域逻辑,从工厂和存储库引用服务和实体是没有意义的。
关于您提供的示例,这就是我将如何实现它
$article->addTag($tag);
$articleRepository->save($article);
我不会直接访问底层集合,因为我可能希望 Article
在将 Tag
添加到集合之前对其执行一些域逻辑(施加约束、验证)。
避免这种情况
$articleService->addTag($article, $tag);
仅使用服务来执行概念上不属于任何实体的操作。首先,尝试将它与实体相匹配,确保它不适合任何实体。然后才使用服务。这样一来,您就不会出现贫乏的领域模型。
更新 1
引用 Eric Evans 的“领域驱动设计:解决软件核心的复杂性”一书:
SERVICES 应谨慎使用,不得剥夺 他们所有行为的实体和价值对象。
更新 2
有人否决了这个答案,我不知道为什么。我只能怀疑原因。它可能与实体和服务之间的引用有关,也可能与示例代码有关。好吧,我对示例代码无能为力,因为这是我根据自己的经验得出的看法。但是,我对参考部分做了更多研究,这就是我想出的。
我不是唯一一个认为从实体引用服务、存储库和工厂不是一个好主意的人。我在 SO 中发现了类似的问题:
Is it ok for entities to access repositories? DDD - Dependecies between domain model, services and repositories DDD - the rule that Entities can't access Repositories directly DDD Entities making use of Services也有一些关于这个主题的好文章,尤其是这篇How not to inject services in entities,它也提供了一个解决方案,如果您迫切需要从您的实体调用一个名为Double Dispatch 的服务。这是移植到 PHP 的文章中的一个示例:
interface MailService
public function send($sender, $recipient, $subject, $body);
class Message
//...
public function sendThrough(MailService $mailService)
$subject = $this->isReply ? 'Re: ' . $this->title : $this->title;
$mailService->send(
$this->sender,
$this->recipient,
$subject,
$this->getMessageBody($this->content)
);
因此,如您所见,您的 Message
实体中没有对 MailService
服务的引用,而是将其作为参数传递给实体的方法。这篇文章的作者“DDD: Services”在cmets部分http://devlicio.us/提出了同样的解决方案。
我还尝试查看 Eric Evans 在他的“领域驱动设计:解决软件核心中的复杂性”一书中对此有何评论。经过简单搜索,我没有找到确切的答案,但我找到了一个实体实际上静态调用服务的示例,即没有引用它。
public class BrokerageAccount
String accountNumber;
String customerSocialSecurityNumber;
// Omit constructors, etc.
public Customer getCustomer()
String sqlQuery =
"SELECT * FROM CUSTOMER WHERE" +
"SS_NUMBER = '" + customerSocialSecurityNumber + "'";
return QueryService.findSingleCustomerFor(sqlQuery);
public Set getInvestments()
String sqlQuery =
"SELECT * FROM INVESTMENT WHERE" +
"BROKERAGE_ACCOUNT = '" + accountNumber + "'";
return QueryService.findInvestmentsFor(sqlQuery);
下面的注释说明了以下内容:
注意:QueryService,一个用于从数据库中获取行的实用程序 和创建对象,对于解释示例很简单,但它不是 对于实际项目来说,这必然是一个好的设计。
如果您查看我上面提到的 DDDSample 项目的源代码,您会发现除了 model
包中的对象(即实体和值对象。顺便说一句,DDDSample 项目在《Domain-Driven Design: Tackling Complexity in the Heart of Software》一书中有详细描述……
另外,我想与您分享的另一件事是关于 domaindrivendesign Yahoo Group。讨论中的这个message 引用了 Eric Evans 关于模型对象引用存储库的主题。
结论
总而言之,从实体中引用服务、存储库和工厂并不好。这是最被接受的意见。尽管存储库和工厂是域层的公民,但它们不是问题域的一部分。有时(例如,在有关 DDD 的 Wikipedia 文章中)域服务的概念称为Pure Fabrication,这意味着类(服务)“不代表问题域中的概念”。我宁愿将工厂和存储库称为 Pure Fabrications,因为 Eric Evans 在他的书中确实提到了关于服务概念的其他内容:
但是当一个操作实际上是一个重要的领域概念时,一个 服务是模型驱动设计的自然组成部分。在宣布 模型作为服务,而不是作为不 实际上代表什么,独立操作不会误导 任何人。
根据上面所说,有时从您的实体调用服务可能是一件理智的事情。然后,您可以使用 Double Dispatch 方法,这样您就不必在您的实体类中保留对服务的引用。
当然,总有一些人像Accessing Domain Services from Entities文章的作者一样不同意主流观点。
【讨论】:
感谢您的帮助。你提到了验证,据我了解,实体和值不应该有任何逻辑,除了代表域。据我了解,验证逻辑应该在工厂中,不是这样吗? 其实这里还有一个有趣的想法。在创作之后,你将如何坚持一些东西?选项 1) 工厂将实体添加到存储库。 2) 您手动将新对象添加到存储库。谢谢。 验证时。我提到它只是作为域逻辑的一个例子,它可以是其他任何东西。无论如何,如果这是领域逻辑所要求的,那么验证应该在实体中进行。如果这是困扰您的问题,则不一定要检查标签的长度或非法字符。有效的标签可能意味着该标签是由文章作者添加的,然后在addTag($tag)
方法中您将检查是否是这种情况。
关于持久性。我会选择选项#2。工厂的唯一职责应该是创建实体。你告诉它创建一个实体,然后由你决定是否要持久化它。
@tounano,使用 ORM 工具,例如 Doctrine。他们将负责持久化您的所有实体。 ORM 工具大量使用了 Identity Map 模式,它跟踪所有加载的实体,因此您不会加载它们两次,以及 Unit Of Work 模式,它跟踪实体的状态,以便您不要坚持没有改变的东西(与您提出的几乎相同)。实体和聚合不应该关心实体的状态,因为这个逻辑不属于域层。【参考方案4】:
应用服务:否 域服务:是,因为它们在域层中 工厂:是,因为它们在域层中 存储库接口:是,因为它们位于域层中 存储库实现:否,因为它们位于基础架构层中实体是否应该知道工厂、存储库、服务 域?
注意接口和实现之间的区别:这就是你应该使用接口和实现的原因。
而且仅供参考,工厂和存储库毕竟是服务,所以你可以概括:
服务接口:是如果它们在域层中简单
域服务是在域层中定义的服务, 尽管实现可能是基础设施层的一部分。一种 存储库是一个域服务,其实现确实在 基础设施层,而工厂也是一个领域服务,其 实现通常在领域层内。
(来源:http://www.methodsandtools.com/archive/archive.php?id=97p2)
存储库应该知道域中的服务吗?
通常不会,毕竟为什么会这样?存储库管理持久性。但我不认为这是“禁止的”,因为基础设施层(持久性)知道领域层。
另一件困扰我的事情是如何对待收藏品 当我想向集合中添加实体时。
尽可能首选 OOP 方法:
$article = new Article();
$article->addTag($tag);
$articleRepository->save($article);
我说得更有意义。
域服务是任何不容易存在于实体中的业务逻辑。
(http://www.methodsandtools.com/archive/archive.php?id=97p2)
也可以:
当域中的重要过程或转换不是实体或值对象的自然责任时,将操作添加到模型中作为声明为服务的独立接口。
(埃里克·埃文斯)
总而言之,如果你觉得需要就创建一个域服务,这不是自动的。
【讨论】:
是什么让代码面向对象?仅仅因为它使用对象和方法并不意味着它是面向对象的。 OOP 是关于抽象和封装的。相反,代码公开了内部数据(tags
集合)及其实现细节。既然客户端代码知道了细节你不能轻易地改变tags
集合的实现,比方说,对于SplFixedArray
实现而不改变客户端代码。但是,如果封装得当($article->addTag($tag)
),就很容易做到。
“仅供参考,工厂和存储库毕竟是服务”不,它们不是,这就是重点。
因为它贬低了拥有单独定义的整个概念。这就像谈论苹果和橙子,然后说好,如果你以不同的方式看待它或画它,苹果可以是橙子。该链接使用术语“域服务”为已经明显复杂的建筑设计哲学添加了一个完整的术语。如果工厂、存储库等是服务,为什么还要使用工厂、存储库等术语呢?
没错,但当你说史密斯奶奶是橙子时,它也会使事情变得混乱。事实上,它类似于说一个橙子(存储库)是一个史密斯奶奶的苹果(域服务)。
服务:当操作在概念上不属于任何域对象或需要多个域对象时。 Repository:检索领域对象的方法。很简单。【参考方案5】:
首先,不要挂断它 - 通过创建不必要的服务类等很容易过度设计。更少的代码是好的。 查看original source material,以及Java 或C# 中的代码。
1) 实体是否应该知道工厂、存储库、服务 域名?
如果需要,它可以。例如,我用@Entity
注释的(java)类也可以用@Configurable
注释,并在其中注入会话/其他类。这就是重点 - 封装所有必要的业务逻辑并公开位于一个域类上的清晰简单的 api。
2) 存储库是否应该知道域中的服务?
没有。但相反的情况很可能,服务会使用存储库。
在使用多个域对象/实体/根聚合时使用服务。所以假设 TAG 是一个单独的实体,这很好:
$articleService->addTag($article, TagEntity);
但是,如果 Tag 不是另一个根聚合,您可以这样做
$article->tags->add(TagEntity);
文章本身通过在其中注入存储库/dao 来进行保存(无需任何其他调用)。
【讨论】:
这个答案是不够的,尤其是第一部分。想象一个提供 addAddress 函数的聚合 UserAddresses。该功能是否也应该保存到存储库?我不这么认为。在我们的 DDD 项目中,DomainModel 模块甚至没有像 Spring 这样的 IoC 框架作为依赖项。域对象中的依赖注入只能通过将依赖作为方法参数传递来实现。此外,当 Aggregate 由 MongoDb 实体等技术构建时,注入依赖项将很困难。您将不得不手动注入它们,这是代码异味²。以上是关于领域驱动设计和 IoC/依赖注入的主要内容,如果未能解决你的问题,请参考以下文章
基于领域式驱动设计(DDD)Ioc(autofac)实现高性能后台框架——StudioCore.1.0
基于领域式驱动设计(DDD)Ioc(autofac)实现高性能后台框架——StudioCore.1.0