我有以下要求:
我试图在这个系统上建立根集合的模型。我的初步设计如下:
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我这里有两个选择:
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的人对延迟加载有很多不同的看法--以下是我发现的关于延迟加载的几点评论:
因此,我并不是在问我是否可以在这里使用延迟加载,而是使用这种方法的缺点是什么(除了破坏一些原则)。我的意思是,你能想象将来它可能会引起一些问题吗?我之所以问这个问题,是因为第二个选择(下面描述)与最终的一致性相比要复杂得多。我知道,如果不破坏需求,最终的一致性并不是坏事,但我还是会选择更简单的解决方案,而不是更复杂的解决方案。
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模式,并将整个推广和取消出版过程转移到不同的地方?无论如何,当使用最终一致性时,这个简单的情况就变得复杂了。此外,当我想到它的时候,它似乎不是事件,最终的一致性,我写的-看起来更像是两阶段提交或类似的?
在这里,我试图找出使用域服务方法的正确方法,我现在是这样想的(简化代码):
域服务:
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)
}
}总根:
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));
}
}聚合存储库:
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基类中的方法)。本质上,域服务被视为域事件(在同一事务中执行),但是它们来自聚合之外,并且它们不“走”在它之外(就像可以在应用层(域层之外)处理的域事件)。
不确定这是否是如何实现域服务,但它似乎可以工作?
发布于 2021-02-13 20:37:20
非常普遍的问题。我过去曾多次面对它。
我的建议(非常固执己见):
提示:我经验性地意识到,我的大多数实体都是聚合根。只有很少的情况下,AR有子实体。
考虑到您的情况,让我们再问一个问题:“直接编辑edition而不通过AR访问它是否有意义?我猜是的(例如更改edition's的促销。我相信促销是按版本进行的,可以通过AR更改)。”
另一个例子:目录有部分,部分有产品。类别的功能产品只能是活动产品(而不是禁用产品)。我会把所有这些都作为一个单独的聚合根。
编辑也考虑DomainEvents https://stackoverflow.com/questions/59583401/correct-way-for-communicating-aggregates-in-ddd
https://softwareengineering.stackexchange.com/questions/422227
复制相似问题