域驱动设计和IoC /依赖注入

时间:2012-12-24 10:38:29

标签: dependency-injection dependencies domain-driven-design inversion-of-control

我正在尝试现在应用我对DDD的了解,我对域模型中依赖关系的流程有点困惑。

我的问题是:

  1. 实体是否应该了解域中的工厂,存储库,服务?
  2. 存储库是否应该知道域中的服务?
  3. 另一件令我烦恼的事情是,当我想要添加集合并将实体添加到集合时,如何处理集合。

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

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

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

    或者我可以通过服务来完成。

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

    您怎么看?

    感谢。

5 个答案:

答案 0 :(得分:20)

实体和价值对象绝不应该依赖于彼此之外的任何东西。这些是building blocks of DDD中最基本的。它们代表了您的问题域的概念,因此应该关注问题。通过使它们依赖于工厂,存储库和服务,您可以使焦点变得模糊。引用实体和值对象中的服务还有另一个问题。因为服务还拥有域逻辑您将很想将域模型的一些职责委托给最终可能导致Anemic Domain Model的服务。

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

关于您提供的示例,这是我实现它的方式

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

我不会直接访问底层集合,因为我可能希望ArticleTag上执行某些域逻辑(强加约束,验证),然后再将其添加到集合。

避免这个

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

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

更新1

Eric Evans"领域驱动设计的引用:解决软件核心的复杂性"书:

  

应谨慎使用服务,不得剥夺服务   他们所有行为的实体和价值目标。

更新2

有人拒绝了这个答案,我不知道为什么。我只能怀疑原因。它可能与实体和服务之间的引用有关,也可能与示例代码有关。好吧,我不能对示例代码做很多事情,因为根据我自己的经验,这是我的观点。但是,我对参考部分进行了更多的研究,这就是我想出来的。

我不是唯一一个认为从实体引用服务,存储库和工厂不是一个好主意的人。我在SO中找到了类似的问题:

还有一些关于这个主题的好文章,特别是这一个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)
        );
    }
}

因此,正如您所看到的,您没有在MailService实体中引用Message服务,而是将其作为参数传递给实体&#39 ; s方法。本文作者提出了相同的解决方案" DDD: Services"在评论部分的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项目的源代码,你会发现除了{{中的对象外,任何实体都没有任何引用。 1}}包,即实体和值对象。顺便说一句,DDDSample项目在"域驱动设计:解决软件核心中的复杂性"详细预订......

另外,我想与你分享的另一件事就是关于 domaindrivendesign Yahoo Group。讨论中的这个message引用了Eric Evans关于引用存储库的模型对象的主题。

<强>结论

总而言之,对实体的服务,存储库和工厂的引用并不好。这是最被接受的意见。即使存储库和工厂是域层的公民,它们也不是问题域的一部分。有时(例如在关于DDD的维基百科文章中)域服务的概念被称为Pure Fabrication,这意味着类(服务)&#34;不代表问题域中的概念&#34;。我更倾向于将工厂和存储库称为纯粹的制造,因为埃里克埃文斯在他的书中对服务的概念做了一些其他的说明:

  

但是当一个操作实际上是一个重要的领域概念时,a   SERVICE是MODEL-DRIVEN DESIGN的自然组成部分。宣告中   模型作为服务,而不是作为一个没有的虚假对象   实际上代表任何东西,独立操作不会误导   任何人。

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

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

答案 1 :(得分:5)

  

实体是否应该了解工厂,存储库,服务   域?

实体永远不应引用存储库或应用程序服务。如果实体使用它来创建组成实体,则实体可以引用它。如果某个实体使用该服务进行某些行为,则该实体也可以依赖域服务。

  

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

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

  

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

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

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

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

答案 2 :(得分:2)

  

实体是否应该了解工厂,存储库,服务   域?

  • 申请服务:
  • 域名服务:,因为它们位于域层
  • 工厂:,因为它们位于域层
  • 存储库接口:,因为它们位于域图层
  • 存储库实施:,因为它们位于基础架构层

注意界面和实现之间的差异:这就是你应该使用interface&amp; amp;的实施方式。

毕竟,工厂和存储库是服务,所以你可以概括:

  • 服务接口:,如果它们位于域图层

  

域服务是在域层中定义的域服务,   虽然实现可能是基础设施层的一部分。一个   repository是一个域服务,其实现确实在   基础设施层,而工厂也是域服务   实现通常在域层内。

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

或者:

  

当域中的重要流程或转换不是ENTITY或VALUE OBJECT的自然责任时,请将模型操作添加为声明为SERVICE的独立接口。

(埃里克埃文斯)

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

答案 3 :(得分:1)

我会以这样的前提条件回答这个问题,即我认为没有正确的答案,只有不同的方法。

根据域对象思考我会说第一种方法是DDD。您正在纯粹处理域对象。

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

然而,这只是一种意见。

答案 4 :(得分:0)

首先,不要陷入困境 - 通过创建不必要的服务类等容易过度工程。更少的代码是好的。 查看original source material以及JavaC#中的代码。

  

1)实体是否应该了解工厂,存储库,服务   域名?

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

  

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

没有。但可能相反,服务将使用存储库。

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

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

但是如果Tag是另一个根聚合,你可以做

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

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