首页
学习
活动
专区
圈层
工具
发布
社区首页 >问答首页 >寻找解决DDD中以下问题的正确途径

寻找解决DDD中以下问题的正确途径
EN

Software Engineering用户
提问于 2021-02-13 20:19:09
回答 1查看 197关注 0票数 0

我有以下要求:

  • 在我的系统中有会议和版本。
  • 每个版本都属于一个会议。
  • 每个会议最多可以有一个当前版本。
  • 每个版本都有草稿或出版的状态。
  • 只有已出版的版本才能是会议的当前版本(它必须在成为属于它的会议的当前版本之前出版)

我试图在这个系统上建立根集合的模型。我的初步设计如下:

代码语言:javascript
复制
class Conference extends RootAggregate {
  static createConference(/* ... */) { /* ... */ }

  guid: Guid;
  editions: Edition[];
  currentEdition: Edition[];

  createEditionDraft() {
    this.editions.push(new Edition({..., status: 'draft' }));
  }

  publishEdition(editionGuid) {
    const edition = this.editions.find(edition => edition.guid === editionGuid);
    edition.status = 'published';
  }

  unpublishEdition(editionGuid) {
    const edition = this.editions.find(edition => edition.guid === editionGuid);
    if (edition.guid == this.currentEditionGuid) {
      throw Error('you can not unpublish current edition');
    }
    edition.status = 'draft';
  }

  setCurrentEdition(editionGuid) {
    const edition = this.editions.find(edition => edition.guid === editionGuid);
    if (edition.status != 'published') {
      throw Error('only published editions can be set as current');
    }
    this.currentEdition = edition;
  }
}

class Edition extends Aggregate {
  guid: Guid;
  status: 'draft' | 'published';
}

这里的所有内容都将如预期的那样工作,因为版本只能通过根聚合( conference )进行更改,而且不可能将草稿设置为当前的会议版本。但是,它需要将所有会议版本加载到会议聚合中。如果有许多版本,它可能会受到性能问题的影响。AFAIK我这里有两个选择:

  1. 不要将所有版本加载到会议聚合中,而是有一种方法,通过id来延迟加载版本,只加载应该设置为当前会议版本的版本。在本例中,代码可能如下所示:
代码语言:javascript
复制
class Conference {
  // ...

  async setCurrentEdition(editionGuid) {
    const edition = await this._loadEdition(editionGuid);
    if (edition.status != 'published') {
      throw Error('only published editions can be set as current');
    }
    this.currentEdition = edition;
  }

  // ...
}

在这种情况下,AFAIK的人对延迟加载有很多不同的看法--以下是我发现的关于延迟加载的几点评论:

  • “一般情况下应该避免”
  • 如果它对你有效,你当然可以!你几乎总是必须打破一些规则。没有理想的解决办法
  • “也许你应该重新考虑你的聚合模型,把它们分成更小的部分,并利用最终的一致性”

因此,我并不是在问我是否可以在这里使用延迟加载,而是使用这种方法的缺点是什么(除了破坏一些原则)。我的意思是,你能想象将来它可能会引起一些问题吗?我之所以问这个问题,是因为第二个选择(下面描述)与最终的一致性相比要复杂得多。我知道,如果不破坏需求,最终的一致性并不是坏事,但我还是会选择更简单的解决方案,而不是更复杂的解决方案。

  1. 拆分会议和版本以分离根聚合并利用最终的一致性:
代码语言:javascript
复制
class Conference extends RootAggregate {
  static createConference(/* ... */) { /* ... * / };
  
  guid: Guid;
  currentEditionGuid: Guid;
  currentEditionCandidateGuid;

  trySetCurrentEdition(editionGuid) {
    if (this.currentEditionCandidateGuid !== null) {
      throw new Error('Another edition is being promoted at the moment');
    }
    this.currentEditionCandidateGuid = editionGuid;
    // CurrentEditionCandidateSetDomainEvent => PrepareEditionToBeSetAsCurrentIntegrationEvent
  }

  // this should run in response to event EditionReadyForPromotionIntegrationEvent
  setCurrentEdition(editionGuid) {
    if (editionGuid !== this.currentEditionCandidateGuid) {
      // this should cause EditionRejectedToBeSetAsCurrentIntegrationEvent
    } else {    
      this.currentEditionGuid = editionGuid;
      this.currentEditionCandidateGuid = null;
      // CurrentEditionSetDomainEvent => CurrentEditionSetIntegrationEvent
    }
  }

  // this should run in response to EditionNotReadyToBePromoted
  clearCurrentEditionCandidate() {
    this.currentEditionCandidateGuid = null;
  }

  // this should run in response to CheckIfEditionIsAllowedToBeUnpublishedIntegrationEvent
  decideIfEditionIsReadyToBeUnpublished(editionGuid) {
    if (this.currentEditionGuid === editionGuid) {
      EditionRejectedToBeUnpublishedIntegrationEvent
    } else {
      EditionAcceptedToBeUnpublishedIntegrationEvent
    }
  }
}

class Edition extends RootAggregate {
  static createEdition(/* ... */) { /* ... * / };

  guid: Guid;
  conferenceGuid: Guid;
  status: 'draft' | 'published';
  promotionInProgress: bool;
  unpublishingInProgress: bool;

  publishEdition() {
    this.status = 'published';
  }

  tryUnpublishEdition() {
    if (this.promotionInProgress || this.unpublishingInProgress) {
      throw new Error('Can not unpublish because it is being promoted or published');
    }
    this.unpublishingInProgress = true;
    // this should cause CheckIfEditionIsAllowedToBeUnpublishedIntegrationEvent
  }

  // this should run in response to event PrepareEditionToBeSetAsCurrentIntegrationEvent
  prepareEditionForPromotion() {
    if (this.status !== 'published' || this.unpublishingInProgress) {
      // this should cause EditionNotReadyToBePromoted
    } else {
      this.promotionInProgress = true;
      // EditionReadyForPromotionDomainEvent => EditionReadyForPromotionIntegrationEvent
    }
  }
  
  // this should run in response to:
  // EditionRejectedToBeSetAsCurrentIntegrationEvent
  // and
  // CurrentEditionSetIntegrationEvent
  stopPromotingEdition() {
    this.promotionInProgress = false;
  }

  // this should run in response to:
  // EditionRejectedToBeUnpublishedIntegrationEvent
  stopUnpublishingEdition() {
    this.unpublishingInProgress = false;
  }
  
  // this should run in response to:
  // EditionAcceptedToBeUnpublishedIntegrationEvent
  unpublishEdition() {
    this.unpublishingInProgress = false;
    this.status = 'draft';
  }
}

正如您所看到的,它更复杂,即使测试单个聚合根很容易,但我认为测试整个过程有点困难。而且,我也不确定这个设计是否正确--一些聚合的“方法”甚至不改变任何状态--它们只是检查状态并导致一些集成事件。我想这些步骤中的一些应该放在不同的(更高的)“层次”,甚至是基于读模型?另外,我想到的是在这里使用Saga模式,并将整个推广和取消出版过程转移到不同的地方?无论如何,当使用最终一致性时,这个简单的情况就变得复杂了。此外,当我想到它的时候,它似乎不是事件,最终的一致性,我写的-看起来更像是两阶段提交或类似的?

编辑:

在这里,我试图找出使用域服务方法的正确方法,我现在是这样想的(简化代码):

域服务:

代码语言:javascript
复制
abstract class DomainService {
  public abstract execute(transactionContext: TransactionContext): Promise<void>;
}

abstract class CheckIfEditionIsReadyToBePromoted extends DomainService {
  editionGuid: Guid;
  
  constructor(editionGuid: Guid) {
    super();
    this.editionGuid = editionGuid;
  }
}

class CheckIfEditionIsReadyToBePromotedImpl extends CheckIfEditionIsReadyToBePromoted {
  constructor(editionGuid: Guid) {
    super(editionGuid);
  }

  execute(transactionContext: TransactionContext) {
    // make use of EditionRepository to read edition by guid and check if its published or not
    // if not raise exception
    // I assume here that root aggregate repository method getByGuid(guid)
    // (which is the only method to read aggregate) out of the box locks reading for aggregate ("select for update" in infrastructure layer)
  }
}

总根:

代码语言:javascript
复制
class Conference {
  current_edition_guid: Guid;

  setCurrentEdition(
    editionGuid: Guid,
    // here we pass domain service to aggregate method 
    checkIfEditionIsReadyToBePromoted: new(editionGuid: Guid) => CheckIfEditionIsReadyToBePromoted
  ) {
    this.current_edition_guid = editionGuid;
    // domain service is added to current "context" same as domain events for example ("this.addDomainEvent(...)")
    this.addDomainService(new checkIfEditionIsReadyToBePromoted(editionGuid));
  }
}

聚合存储库:

代码语言:javascript
复制
class ConferenceRepository {
  transactionContext: TransactionContext;
  // ...
  async saveChanges() {
    // save aggregate changes
    // dispatchDomainEvents
    // executeDomainServices <= not sure if those should be executed at the end or beginning of this method
  }
  // ...
}

这样可以方便地测试会议,保持域服务抽象,有一个简单的方法将域服务传递给聚合体(addDomainService可以是AggregateRoot基类中的方法)。本质上,域服务被视为域事件(在同一事务中执行),但是它们来自聚合之外,并且它们不“走”在它之外(就像可以在应用层(域层之外)处理的域事件)。

不确定这是否是如何实现域服务,但它似乎可以工作?

EN

回答 1

Software Engineering用户

发布于 2021-02-13 20:37:20

非常普遍的问题。我过去曾多次面对它。

我的建议(非常固执己见):

  1. 如果您知道,每个会议最多只有几十个版本,而不是延迟加载(但尽量避免N+1问题)。最终,从DB加载50-100条记录时,只有在编辑某些内容并不是过度时才会这样做。
  2. 如果上述情况并非如此(例如,聚合根下有数千个实体),那么也可以考虑将会议设置为聚合根,并使用DomainService的概念。我对DomainService的理解是,它包含不属于任何AR的逻辑(在您的例子中,将有同时处理两个AR的逻辑--会议和版本)。然后,您只能使用所需的聚合(例如当前会议、当前版本、新版本)调用DomainService逻辑,这将消除获取会议的所有版本的需要。

提示:我经验性地意识到,我的大多数实体都是聚合根。只有很少的情况下,AR有子实体。

考虑到您的情况,让我们再问一个问题:“直接编辑edition而不通过AR访问它是否有意义?我猜是的(例如更改edition's的促销。我相信促销是按版本进行的,可以通过AR更改)。”

另一个例子:目录有部分,部分有产品。类别的功能产品只能是活动产品(而不是禁用产品)。我会把所有这些都作为一个单独的聚合根。

编辑也考虑DomainEvents https://stackoverflow.com/questions/59583401/correct-way-for-communicating-aggregates-in-ddd

票数 1
EN
页面原文内容由Software Engineering提供。腾讯云小微IT领域专用引擎提供翻译支持
原文链接:

https://softwareengineering.stackexchange.com/questions/422227

复制
相关文章

相似问题

领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档