TypeScript中聚合设计的挑战之领域驱动设计

79 阅读4分钟

这是我最近收到的一个问题。

问:一个领域模型如何引用一个不同子域的模型?

提问的人也给了我一些关于他们实际问题的细节。

Order.create(userId)创建了一个订单,只有当User.getReputation()是 "可信任的 "时,这个订单才会被接受。如果发货的产品超过10件,用户的信誉就是可信的,否则就会返回失败的结果。

好吧,让我们深入了解一下。

我将假设我们是在某种电子商务平台上工作,包含一个Users 子域和一个Shipping

用户子域

Users 子域中,让我们假设我们有一个聚合根为User

interface UserProps {
  email: UserEmail;
  password: UserPassword;
}

class User extends AggregateRoot<UserProps> {
  private constructor (props: UserProps, id?: UniqueEntityID) {
    super(props, id);
  }
  
  public create (props: UserProps, id?: UniqueEntityID): <User> {
    const guardResult = Guard.againstNullOrUndefinedBulk([
      { argument: props.email, arguementName: 'email' },
      { argument: props.password, arguementName: 'password' }
    ]);

    if (guardResult.isFailure) {
      return Result.fail<User>(guardResult.error);
    }

    return Result.ok<User>(new User(props, id));
  }
}

我正在考虑user.getReputation() 的方法。在User 实体上,这是否有意义?

reputation 甚至在Users 子域中也是一个关注点吗?

我个人认为不是这样。

这就是原因。

聚合边界

在Vaughn Vernon的DDD书中,他说。

"当我们试图在一个有边界的上下文中发现聚合体时,我们必须了解该模型的真正不变性。只有掌握了这些知识,我们才能确定哪些对象应该被聚类到一个给定的聚合体中。"- 摘自。Vernon, Vaughn."实现领域驱动设计"。

我认为在计算reputation 的责任属于哪个实体方面有一点混淆。

我们的本能是认为一切都由User ,因为Users ,主要是我们对使用我们系统的人的思考方式。

但在DDD中,我们必须考虑得更细一些。特别是当有几个子域的时候。

在给出的例子中,reputation 显然是由User 聚合计算出来的。然而,该计算只能通过计算 "已发货产品 "的数量来确定。

Products 是不在User 聚合体的一致性边界内的。 实体并不包含对 集合的引用。而且它也不应该需要这样。User products

通过将用例分配给子域来确定责任

还记得本文中题为**"我们通过对话发现用例**"的部分,我们发现Users 子域只负责身份和访问管理,包含的用例是:。

  • login(userEmail: UserEmail, password: UserPassword)
  • logout(authToken: JWTToken)
  • verifyEmail(emailVerificationToken: EmailVerificationToken)
  • changePassword(passwordResetToken: Token, password: UserPassword)

我认为我们缺少的是Shipping 子域中的Merchant 集合,像这样。

type ReputationType = 'unranked' | 'poor' | 'good' | 'great';

interface MerchantProps {
  products: WatchedList<Product>;
  shippedProducts: WatchedList<Product>;
  reputation: ReputationType;
}

export class Merchant extends AggregateRoot<MerchantProps> {
  ...
}

从这个子域中,我希望能看到更多的用例,比如:。

  • shipProduct(product: Product)
  • getMerchantById(merchantId: MerchantId)

计算Merchant 的声誉

这就解决了我们试图找出如何从一个单独的子域引用一个实体的问题(我们通常在正确的情况下对应用服务/用例进行引用)。

但是现在,我们如何计算ReputationType

乍一看,明显的方法是像这样统计所有发货的产品。

export class Merchant extends AggregateRoot<MerchantProps> {
  ...
  public getReputation (): ReputationType {
    const numProductsShipped = products.getItems()
      .reduce((curr, product) => !!e.shipped ? curr + 1 : curr, 0);

    if (shipped < 5) {
      return 'unranked'
    } else if (shipped >= 5) {
      // etc and so on.
    }
  }
}

但是,由于MerchantProduct 有一个 0-to-many 的关系,这种无界的关系最终可能跨越数百或数千个products

这意味着我们在检索Merchant 时,必须确保从 repo 中检索到所有的产品......总是如此。

这并不理想。

这里有一个更好的方法。

在每个ProductShippedEvent 域事件后更新Reputation

由于Product 是在Merchant 的一致性边界之下,我们可以给Merchant 添加一个方法来标记一个product 为已发货。

export class Merchant extends AggregateRoot<MerchantProps> {
  public shipProduct (product: Product): void {
    
    // Add the item to shipped products so that the 
    // Product repo can save it as updated when we
    // persist the domain entity. 
    this.shippedProducts.addItem(product);

    // Create a domain event for the product shipment
    this.addDomainEvent(new ProductShippedEvent(product))
  }
}

然后,从同一个子域中,我们可以订阅域名事件。

// src/modules/shipping/subscribers

export class AfterProductShippedEvent implements IHandle<ProductShippedEvent> {
  private merchantRepo: IMerchantRepo;
  private productRepo: IProductRepo;

  constructor (merchantRepo: IMerchantRepo, productRepo: IProductRepo) {
    this.merchantRepo = merchantRepo;
    this.productRepo = productRepo;

    DomainEvents.register(this.onProductShippedEvent.bind(this), ProductShippedEvent.name);
  }

  private onProductShippedEvent (event: ProductShippedEvent) : Promise<any> {
    const numberShippedProducts: number = await this
      .productRepo.countNumberShippedProducts(event.product.merchantId);
    
    // Use a domain service to calculate the reputation.
    // We want to ensure that we encapsulate as much domain logic
    // in the domain layer as possible.

    let newMerchantReputation: ReputationType = MerchantReputationService
      .calculateReputation(numberShippedProducts);

    // Set the reputation
    await this.merchantRepo
      .setMerchantReputation(event.product.merchantId, newMerchantReputation);
  }
}

这就是我们所说的最终一致性,它属于与CQRS相关但不受限于CQRS的主题范围。