14

我现在正在尝试应用我学到的关于 DDD 的知识,但我对域模型中的依赖关系流有点困惑。

我的问题是:

  1. 实体是否应该知道域中的工厂、存储库、服务?
  2. 存储库是否应该知道域中的服务?

困扰我的另一件事是当我想向集合添加实体时如何处理集合。

假设我正在开发一个简单的 CMS。在 CMS 中,我有一个包含标签实体的文章实体和标签集合。

现在,如果我想添加一个带有新标签的关系。更好的方法是什么?(PHP 中的示例)

$article->tags->add(TagEntity);
$articleRepository->save($article);

或者我可以通过服务来做到这一点。

$articleService->addTag($article, TagEntity);

你怎么看?

谢谢。

4

5 回答 5

29

实体和值对象不应该相互依赖。这些是DDD 所有构建块中最基本的。它们代表了您的问题域的概念,因此应该关注问题。通过使它们依赖于工厂、存储库和服务,您会使焦点变得模糊。在实体和值对象中引用服务还有另一个问题。因为服务也拥有领域逻辑你会倾向于将领域模型的一些职责委托给服务,这最终可能导致贫血的领域模型

工厂和存储库只是用于创建和持久化实体的助手。大多数时候,它们只是在真正的问题域中没有类比,因此根据域逻辑,从工厂和存储库引用服务和实体是没有意义的。

关于您提供的示例,这就是我将如何实现它

$article->addTag($tag);
$articleRepository->save($article);

我不会直接访问底层集合,因为我可能希望在将其添加到集合之前Article对其执行一些域逻辑(施加约束、验证) 。Tag

避免这种情况

$articleService->addTag($article, $tag);

仅使用服务来执行概念上不属于任何实体的操作。首先,尝试将它与实体相匹配,确保它不适合任何实体。然后才使用服务。这样你就不会得到贫血的域模型。

更新 1

引用 Eric Evans 的“领域驱动设计:解决软件核心中的复杂性”一书的话:

应该明智地使用服务,并且不允许剥夺实体和价值对象的所有行为。

更新 2

有人对这个答案投了反对票,我不知道为什么。我只能怀疑原因。它可能与实体和服务之间的引用有关,也可能与示例代码有关。好吧,我对示例代码无能为力,因为这是我根据自己的经验得出的看法。但是,我对参考资料部分做了更多研究,这就是我想出的。

我不是唯一一个认为从实体引用服务、存储库和工厂不是一个好主意的人。我在这里发现了类似的问题:

还有一些关于这个主题的好文章,特别是这篇How not to injection services in entity也提供了一个解决方案,如果你迫切需要从你的实体调用一个名为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)
        );
    }
}

因此,如您所见,您没有对实体MailService内部服务的引用Message,而是将其作为参数传递给实体的方法。这篇文章“ DDD:服务”的作者在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 是一个用于从数据库中获取行并创建对象的实用程序,用于解释示例很简单,但对于实际项目而言,它不一定 是一个好的设计。

model如果您查看我上面提到的 DDDSample 项目的源代码,您会发现 Entities 除了包中的对象(即实体和值对象)之外没有任何引用。顺便说一句,DDDSample 项目在《Domain-Driven Design: Tackling Complexity in the Heart of Software》一书中有详细描述……

另外,我想与您分享的另一件事是关于 domaindrivendesign Yahoo Group的类似讨论。讨论中的这条消息引用了 Eric Evans 关于模型对象引用存储库的主题。

结论

总而言之,从实体中引用服务、存储库和工厂并不好。这是最被接受的意见。尽管存储库和工厂是域层的公民,但它们不是问题域的一部分。有时(例如,在有关 DDD 的 Wikipedia 文章中)域服务的概念被称为Pure Fabrication,这意味着类(服务)“不代表问题域中的概念”。我宁愿将工厂和存储库称为 Pure Fabrications,因为 Eric Evans 在他的书中确实提到了关于服务概念的其他内容:

但是当操作实际上是一个重要的领域概念时,服务就形成了模型驱动设计的自然部分。在模型中声明为服务,而不是作为实际上不代表任何东西的虚假对象,独立操作不会误导任何人。

根据上面所说,有时从您的实体调用服务可能是一件明智的事情。然后,您可以使用 Double Dispatch 方法,这样您就不必在您的实体类中保留对服务的引用。

当然,总有一些人不同意主流观点,比如Accessing Domain Services from Entities一文的作者。

于 2012-12-25T11:02:45.960 回答
5

实体是否应该知道域中的工厂、存储库、服务?

实体永远不应引用存储库或应用程序服务。如果实体使用工厂来创建组成实体,那么实体引用工厂是可以接受的。如果实体将域服务用于某些行为,那么它对域服务的依赖也是可以接受的。

存储库是否应该知道域中的服务?

一般没有。存储库应该只负责持久性。

现在,如果我想添加一个带有新标签的关系。更好的方法是什么?

这取决于您指的是哪一层。在典型的 DDD 架构中,您将拥有两段代码。您将拥有一个文章应用程序服务,它封装了域并提供了一种细粒度的方法,例如addTag您将在哪里传递文章 ID 和标签 ID。此方法将检索适当的文章和标签实例(如果需要),然后:

$article->tags->add(TagEntity);
$articleRepository->save($article);

依赖于该域的所有外层都将通过应用程序服务与其通信。

于 2012-12-24T17:22:33.167 回答
3

实体是否应该知道域中的工厂、存储库、服务?

  • 申请服务:
  • 领域服务:的,因为它们在领域层
  • 工厂:的,因为它们在领域层
  • 存储库接口:的,因为它们在领域层
  • 存储库实现:,因为它们在基础设施层

注意接口和实现之间的区别:这就是你应该使用接口和实现的原因。

仅供参考,工厂和存储库毕竟是服务,因此您可以概括:

  • 服务接口:的,如果它们在域层中

简单的

域服务是在域层中定义的服务,尽管实现可能是基础设施层的一部分。存储库是一个领域服务,它的实现确实在基础设施层,而工厂也是一个领域服务,它的实现通常在领域层内。

(来源: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)

或者也:

当域中的重要过程或转换不是实体或值对象的自然责任时,将操作添加到模型中作为声明为服务的独立接口。

(埃里克·埃文斯)

总而言之,如果您觉得需要,请创建域服务,这不是自动的。

于 2012-12-27T16:19:53.557 回答
1

我将在我认为没有正确答案的前提下回答这个问题,只是方法不同。

从域对象的角度考虑,我会说第一种方法是 DDD。您纯粹是在处理域对象。

但是,我确实认为服务对象有一些用途,它将其作为 API/服务层的一部分公开。在这种情况下,您可以将#1 中的代码包装在您的服务#2 中。这允许您避免将域对象暴露给外部消费者 - 并且您可以在更新域模型时保持外部接口/API 不变。

然而,这只是一种观点。

于 2012-12-24T11:15:35.540 回答
0

首先,不要挂断它 - 通过创建不必要的服务类等很容易过度设计。更少的代码是好的。 查看原始源材料以及JavaC#中的代码。

1) 实体是否应该知道域中的工厂、存储库、服务?

如果需要,它可以。例如,我的(java)类@Entity也可以用注释来注释,@Configurable并将会话/其他类注入其中。这就是重点 - 封装所有必要的业务逻辑并公开位于一个域类上的清晰简单的 api。

2)存储库是否应该知道域中的服务?

不可以。但很可能相反,服务会使用存储库。

当使用多个域对象/实体/根聚合时使用服务。因此,假设 TAG 是一个单独的实体,这很好:

$articleService->addTag($article, TagEntity); 

但是,如果 Tag 不是另一个根聚合,您可以这样做

$article->tags->add(TagEntity);

并且文章本身通过在其中注入存储库/ dao 来进行保存(无需任何其他调用)。

于 2012-12-28T13:07:02.507 回答