应用程序或域服务中的DDD存储库


29

这些天,我正在学习DDD,并且对如何使用DDD管理存储库有一些疑问。

实际上,我遇到了两个可能性:

第一

我读过的管理服务的第一种方法是在应用程序服务中注入存储库和域模型。

这样,在一种应用程序服务方法中,我们称为域服务方法(检查业务规则),如果条件良好,则在特殊方法上调用存储库以从数据库中持久化/检索实体。

一个简单的方法可以是:

class ApplicationService{

  constructor(domainService, repository){
    this.domainService = domainService
    this.repository = repository
  }

  postAction(data){
    if(this.domainService.validateRules(data)){
      this.repository.persist(new Entity(data.name, data.surname))
    }
    // ...
  }

}

第二个

第二种可能性是将存储库注入domainService内,并且仅通过域服务使用存储库:

class ApplicationService{

  constructor(domainService){
    this.domainService = domainService
  }

  postAction(data){
    if(this.domainService.persist(data)){
      console.log('all is good')
    }
    // ...
  }

}

class DomainService{

  constructor(repository){
    this.repository = repository
  }

  persist(data){
    if(this.validateRules(data)){
      this.repository.save(new Entity(data.name))
    }
  }

  validateRules(data){
    // returns a rule matching
  }

}

从现在开始,我无法区分哪个是最好的(如果有的话)或它们在上下文中所暗示的含义。

你能给我一个例子,一个可以比另一个更好的原因,为什么?



“在应用程序服务中注入存储库和域模型。” 在某处注入“域模型”是什么意思?就DDD域模型而言,AFAICT意味着该域的整个概念集以及与应用程序相关的它们之间的交互。这是抽象的东西,不是内存中的对象。您不能注入它。
Alexey

Answers:


31

简短的答案是-您可以使用应用程序服务或域服务中的存储库-但是考虑这样做的原因和方式非常重要。

域服务的目的

域服务应封装域概念/逻辑-同样,域服务方法:

domainService.persist(data)

不属于域服务,因为persist它不是普适语言的一部分,并且持久性操作也不是域业务逻辑的一部分。

通常,当您具有需要协调或使用多个聚合的业务规则/逻辑时,域服务非常有用。如果逻辑仅涉及一个聚合,则应采用该聚合实体上的方法。

应用程序服务中的存储库

因此从这个意义上讲,在您的示例中,我更喜欢您的第一个选择-但是即使您的域服务正在接受来自api的原始数据,但即使还有改进的余地-为什么域服务也应该了解data?的结构?此外,数据似乎仅与单个聚合有关,因此使用域服务的价值有限-通常,我会将验证放入实体构造函数中。例如

postAction(data){

  Entity entity = new Entity(data.name, data.surname);

  this.repository.persist(entity);

  // ...
}

并在无效时引发异常。根据您的应用程序框架,拥有一种用于捕获异常并将其映射到api类型的相应响应的一致机制可能很简单-例如,对于REST api,返回400状态代码。

域服务中的存储库

尽管有上述规定,有时在域服务中注入和使用存储库还是有用的,但前提是您的存储库实现为仅接受和返回聚合根,并且在抽象包含多个聚合的逻辑时也是如此。例如

postAction(data){

  this.domainService.doSomeBusinessProcess(data.name, data.surname, data.otherAggregateId);

  // ...
}

域服务的实现如下所示:

doSomeBusinessProcess(name, surname, otherAggregateId) {

  OtherEntity otherEntity = this.otherEntityRepository.get(otherAggregateId);

  Entity entity = this.entityFactory.create(name, surname);

  int calculationResult = this.someCalculationMethod(entity, otherEntity);

  entity.applyCalculationResultWithBusinessMeaningfulName(calculationResult);

  this.entityRepository.add(entity);

}

结论

这里的关键是域服务封装了一种过程,该过程是无处不在的语言的一部分。为了履行其职责,它需要使用存储库-这样做是完全可以的。

但是,添加一个使用称为的方法来包装存储库的域服务persist几乎没有什么价值。

在此基础上,如果您的应用程序服务所表达的用例仅要求使用单个聚合,则直接从应用程序服务使用存储库没有问题。


好吧,因此,如果我拥有业务规则(允许使用规范模式规则),并且仅涉及一个实体,那么我应该在那个实体中进行验证吗?似乎很奇怪地要注入业务规则,例如在用户实体内部控制良好的用户邮件格式。是不是 关于全球回应,谢谢。它得出结论,没有“适用的默认规则”,它实际上取决于我们的用例。我要做一些工作来很好地区分所有工作
mfrachet

2
为了明确起见,属于该实体的规则仅是该实体负责的规则。我同意,控制良好的用户电子邮件格式并不觉得它属于User实体。就个人而言,我喜欢将类似的验证规则放入代表电子邮件地址的Value Object中。用户将具有类型为EmailAddress的属性,并且EmailAddress构造函数接受字符串,如果字符串与所需格式不匹配,则引发异常。然后,您可以在需要存储电子邮件地址的其他实体上重复使用EmailAddress ValueObject。
克里斯·西蒙

好的,我明白了为什么现在要使用Value Object。但这意味着值对象拥有一个属性,该属性是管理格式的业务规则吗?
mfrachet

1
值对象应该是不可变的。通常,这意味着您在构造函数中进行初始化和验证,并且对于任何属性,请使用public get / private设置模式。但是您可以使用语言构造来定义相等性,ToString过程等,例如kacper.gunia.me/ddd-building-blocks-in-php-value-objectgithub.com/spring-projects/spring-gemfire-examples/ blob / master /…
克里斯·西蒙

最后,谢谢@ChrisSimon,回答一个涉及代码而不只是理论的现实DDD情况。我花了5天时间在SO和Web上拖拉,以创建和保存聚合的功能示例,这是我找到的最清楚的解释。
e_i_pi

2

接受的答案有问题:

域模型不允许依赖存储库,并且域服务是域模型的一部分-> 域服务不应依赖存储库。

相反,您应该做的是组装应用程序服务中已经存在的执行业务逻辑所需的所有实体,然后只为模型提供实例化的对象。

根据您的示例,它看起来可能像这样:

class ApplicationService{

  constructor(domainService, repository){
    this.domainService = domainService
    this.repositoryA = repositoryA
    this.repositoryB = repositoryB
    this.repositoryC = repositoryC
  }

  // any parsing and/or pre-business validation already happened in controller or whoever is a caller
  executeUserStory(data){
    const entityA = this.repositoryA.get(data.criterionForEntityA)
    const entityB = this.repositoryB.get(data.criterionForEntityB)

    if(this.domainService.validateSomeBusinessRules(entityA, entityB)){
      this.repositoryC.persist(new EntityC(entityA.name, entityB.surname))
    }
    // ...
  }
}

因此,经验法则:域模型不依赖于外部层

应用程序vs域服务 来自本文

  • 域服务非常细粒度,其中应用程序服务是旨在提供API的立面。

  • 域服务包含不能自然地放置在实体或值对象中的域逻辑,而应用程序服务编排域逻辑的执行,并且它们本身并不实现任何域逻辑。

  • 域服务方法可以将其他域元素用作操作数和返回值,而应用程序服务则对琐碎的操作数(例如标识值和原始数据结构)进行操作。

  • 应用程序服务声明对执行域逻辑所需的基础结构服务的依赖性。


1

除非您的服务和对象封装了一些连贯的责任,否则这两种模式都不是好的。

首先说出您的领域对象是什么,然后谈谈它在领域语言中可以做什么。如果它可以有效或无效,为什么不将其作为域对象本身的属性?

例如,如果对象有效性仅对另一个对象有意义,那么您可能有一个责任“域对象的验证规则X”,可以将其封装在一组服务中。

验证对象是否需要将其存储在业务规则中?可能不会。“存储对象”的责任通常在单独的存储库对象中。

现在,您要执行的操作涵盖了一系列职责,创建对象,对其进行验证以及(如果有效)进行存储。

此操作是域对象固有的吗?然后使其成为该对象的一部分,即ExamQuestion.Answer(string answer)

它是否适合您网域的其他部分?放在那里Basket.Purchase(Order order)

您愿意使用ADM REST服务吗?那好吧。

Controller.Post(json) 
{ 
    parse(json); 
    verify(parsedStruct); 
    save(parsedStruct); 
    return 400;
}
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.