领域驱动设计和 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/依赖注入的主要内容,如果未能解决你的问题,请参考以下文章

前端工程化-在React中对DDD领域驱动架构设计的实践

前端工程化-在React中对DDD领域驱动架构设计的实践

基于领域式驱动设计(DDD)Ioc(autofac)实现高性能后台框架——StudioCore.1.0

基于领域式驱动设计(DDD)Ioc(autofac)实现高性能后台框架——StudioCore.1.0

基于领域式驱动设计(DDD)Ioc(autofac)实现高性能后台框架——StudioCore.1.0

领域驱动设计中的层