领域事件在哪里被创建?| 领域驱动设计与TypeScript

233 阅读4分钟

上一次,我们谈到了如何使用Repository模式在我们的ORM上创建一个门面。今天,我们要谈一谈我们在哪里创建领域事件。

领域事件是领域驱动设计的一个重要部分。

如果实施得当,它们允许我们使用观察者模式(这是一种众所周知的分离关注点和解耦异步逻辑的方法)来连锁领域逻辑,保持单一责任,并跨越架构边界(子域)。

问题是:"领域事件在哪里被创建"?

领域事件是领域层的一部分,所以它们属于所有其他领域层结构,如实体价值对象领域服务

好的,但它们到底是在哪里被创建的呢?

就在聚合根上

请允许我解释一下。

对我来说,领域驱动设计最美妙的地方在于它敦促你创建你自己的领域专用语言(DSL)

你只公开那些对领域有意义的方法和属性。而这些方法和属性只属于那些有意义的实体价值对象

class User {
  // factory method to create users, yep
  public static create (props): User { }  

  // yup, also a valid domain operation
  public deactivate (): void {}       
  
  // ❌ No. A client shouldn't need to use this operation.
  // Use a Value Object to encapsulate validation logic instead: 
  // https://khalilstemmler.com/articles/typescript-value-object/  
  public validateFirstName (firstName): boolean {}            
}

确定什么有意义很难的。它通常需要与领域专家进行大量的对话,并对模型进行多次迭代。

这种对YAGNI重要性的增加,以及只公开对模型有效的操作,也是为什么getters和setters在领域驱动设计中被大量使用的一个重要原因。

使用getters和setters,我们不仅可以准确地指定允许访问/更改的内容,而且还可以指定何时允许访问/更改,以及访问/更改时发生什么

基本属性被访问或改变时,创建一个域事件可能是有意义的。

例如,如果我们正在研究White Label的功能,使Traders接受拒绝 Offers ,我们就必须走过这个过程,确定Domain Logic应该属于哪里

按照这个过程,我们会发现acceptOffer()declineOffer()Offer 聚合根本身进行突变。

这些操作可以在没有任何其他领域实体参与的情况下完成,所以把它们直接放在Offer 聚合根上是合理的。

export type OfferState = 'initial' | 'accepted'  | 'declined'

interface OfferProps {
  ...
  state: OfferState;
}

export class Offer extends AggregateRoot<OfferProps> {
  ...

  get offerId (): OfferId {
    return OfferId.create(this._id);
  }

  get offerState (): OfferState {
    return this.props.state;
  }

  public acceptOffer (): Result<any> {
    switch (this.offerState) {
      case 'initial':
        // Notice how there is not a public setter for the
        // 'state' attribute. That's because it's important that
        // we intercept changes to state so that we can create and add
        // a Domain Event to the "observable subject" when it's
        // appropriate to do so.
        this.props.state = 'accepted';
        // And then we create the domain event.
        this.addDomainEvent(new OfferAcceptedEvent(this.offerId));
        return Result.ok<any>();
      case 'accepted':
        return Result.fail<any>(new Error('Already accepted this offer'));
      case 'declined':
        return Result.fail<any>(new Error("Can't accept an offer already declined"));
      default:
        return Result.fail<any>(new Error("Offer was in an invalid state"));
    }
  }

  public declineOffer (): Result<any> {
    switch (this.offerState) {
      case 'initial':
        // Same deal is going on here.
        this.props.state = 'declined';
        this.addDomainEvent(new OfferDeclinedEvent(this.offerId));
        return Result.ok<any>();
      case 'accepted':
        return Result.fail<any>(new Error('Already accepted this offer'));
      case 'declined':
        return Result.fail<any>(new Error("Can't decline an offer already declined"));
      default:
        return Result.fail<any>(new Error("Offer was in an invalid state"));
    }
  }

  private constructor (props: OfferProps, id?: UniqueEntityId) {
    super(props, id);
  }
}

而如果我们采取挂钩的用例方法,执行这个功能会看起来像。

export class AcceptOfferUseCase implements UseCase<AcceptOfferDTO, Result<Offer>> {
  private offerRepo: IOfferRepo;
  private artistRepo: IArtistRepo;

  constructor (offerRepo: IOfferRepo) {
    this.offerRepo = offerRepo
  }

  public async execute (request: AcceptOfferDTO): Promise<Result<Offer>> {
    const { offerId } = request;
    const offer = this.offerRepo.findById(offerId);

    if (!!offer === false) {
      return Result.fail<Offer>(ErrorType.NOT_FOUND)
    }

    // Creates the domain event
    offer.acceptOffer();

    // Persists the offer and dispatches all created domain events
    this.offerRepo.save(offer);
    
    return Result.ok<Offer>(offer)
  }
}

因为域事件是域的一部分,我们总是想尽量把域事件放在靠近实体/聚合根的地方

如果我们可以相信域层在那些关键场景中总是会生成适当的域事件,我们就可以让几个应用程序(也许部署在独立的边界上下文中,并使用 RabbitMQ 这样的消息队列在网络上传播)实现它们自己的不同的应用层逻辑,以便随意处理这些事件。

例如,如果我们的Billing 子域要订阅OfferAcceptedEvent ,我们可能想通过促进双方之间的交易并收取0.2%的处理费来完成交易。