DDD 和定义聚合

DDD and defining aggregates

我们正在构建一个系统,可以将我们的 api 服务出售给多家公司。 我们有

在基础设施上,它看起来像这样:

"company1" : {
    "accounts" : [
       account1 :{"users" : [{user1,user2}], accountType},
       account2 :{"users" : [{user1,user2}], accountType},
]}

其中一条业务规则规定用户在注册后不能更改帐户。 其他规则规定用户可以更改他的类型,但只能在该帐户类型内。

根据我的理解,我的域模型应该称为 UserAccount,它应该由 Account、User 和 UserType 实体组成,其中 Account 将是聚合根。

class UserAccount{
  int AccountId;
  string AccountName;
  int AccountTypeId;
  List<UserTypes> AvailableUserTypesForThisAccount
  User User
  void SetUserType(userTypeId){
      if(AvailableUserTypesForThisAccount.Contains(userTypeId) == false)
         throw new NotSupportedException();

  }

}

使用此聚合,我们可以更改用户的类型,但它只能是该帐户可用的类型(不变量之一)。

当我从存储库获取 UserAccount 时,我会获取所有必要的表(或实体数据对象)并将它们映射到聚合,然后将其作为一个整体返回。

我的理解和建模是否朝着正确的方向发展?

理解聚合的设计权衡很重要;因为聚合将您的领域模型划分为独立的空间,所以您可以同时修改模型的不相关部分。但是您无法在变更点.

执行跨越多个聚合的业务规则

这意味着你需要清楚地了解这两件事的商业价值。对于不会经常更改的实体,您的企业可能更喜欢严格执行而不是并发更改;在数据经常更改的地方,您最终可能会更喜欢更多的隔离。

在实践中,隔离意味着评估企业是否有能力缓解 "conflicting" 编辑使模型处于不满意状态的情况。

With this aggregate, we can change type of the user, but it can only be type which is available for that account (one of invariants).

对于这样的不变量,要问的一个重要问题是 "what is the business cost of a failure here"?

如果 UserAccount 是单独的聚合,那么您将面临一个问题,即一个用户被分配到 "type",同时一个帐户正在放弃对那种。那么,(在更改之后)检测到违反 "invariant" 的情况发生了多少费用,应用更正的费用是多少?

如果 Account 相对稳定(看起来很可能),则可以通过将用户类型与帐户中允许的用户类型的缓存列表进行比较来减轻大部分错误。可以在更改用户时或在支持编辑的 UI 中评估此缓存。这将减少(但不会消除)错误率而不影响并发编辑。

From my understanding, my domain model should be called UserAccount, and it should consist of Account, User and UserType entities, where Account would be aggregate root.

我认为你在这里失去了情节。 "domain model" 并不是一个真正命名的东西,它只是聚合的集合。

如果您想要一个包含用户和用户类型的帐户聚合,那么您可能会对其进行建模

Account : Aggregate {
    accountId : Id<Account>,
    name : AccountName,
    users : List<User>,
    usertypes : List<UserType>
}

这种设计意味着对用户的所有更改都需要通过帐户聚合访问,并且没有用户属于多个帐户,并且没有其他聚合可以直接引用用户(您需要直接协商与帐户聚合)。

Account::SetUserType(UserHint hint, UserType userTypeId){
    if(! usertypes.Contains(userTypeId)) {
        throw new AccountInvariantViolationException();
    }
    User u = findUser(users, hint);
    ...
}

When I fetch UserAccount from repository, I would fetch all necessary tables (or entity data objects) and mapped them to aggregate, and returned it as a whole.

是的,这是完全正确的——这是我们通常更喜欢 小型 松散耦合聚合而不是一个大型聚合的另一个原因。

what about having only the relationship between Account and User live in the Account aggregate as well as the type of user (as an AccountUser entity) and have the rest of the user information live in a separate User aggregate?

该模型可能适用于某些类型的问题——在这种情况下,帐户聚合可能类似于

Account : Aggregate {
    accountId : Id<Account>,
    name : AccountName,
    users : Map<Id<User>,UserType>
    usertypes : List<UserType>
}

此设计允许您在某些用户当前属于该类型时尝试从帐户中删除用户类型时抛出异常。但是,例如,它不能确保此处描述的用户类型实际上与独立用户聚合的状态一致——或者事件确定已识别的用户存在(对于这些情况,您将依赖检测和缓解) .

这样更好吗?更差?如果没有对正在解决的实际问题有更透彻的了解,就不可能这么说(试图从玩具问题中理解 真的很难)。

原则是了解必须始终保持哪些业务不变量(相对于那些可以接受后期协调的业务不变量),然后将所有必须保持一致的状态组合在一起以满足不变量.

But what if account can have hundreds or thousands of users? What would be your vision of aggregate?

假设相同的约束:我们有一些聚合负责允许的用户类型范围....如果聚合太大而无法以合理的方式管理,以及由业务不能放松,那么我可能会妥协 "repository" 抽象,并允许执行设置的验证规则泄漏到数据库本身。

DDD 的自负源于其最初的 OO 最佳实践根源,即模型是真实的,而持久性存储只是一个环境细节。但是用实际的眼光来看,在一个流程有生命周期并且有竞争消费者的世界中......它是代表业务真相的持久性存储。