DDD和定义聚合

Rob*_*ert 2 domain-driven-design aggregateroot

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

  • 公司(购买我们api的公司)
  • 帐户(每个公司可以有多个帐户,每个帐户都有其用户类型)
  • 用户(帐户内的用户)

从基础上看,它看起来像这样:

"company1" : {
    "accounts" : [
       account1 :{"users" : [{user1,user2}], accountType},
       account2 :{"users" : [{user1,user2}], accountType},
]}
Run Code Online (Sandbox Code Playgroud)

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

据我了解,我的域模型应称为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();

  }

}
Run Code Online (Sandbox Code Playgroud)

使用此汇总,我们可以更改用户的类型,但只能是可用于该帐户的类型(不变式之一)。

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

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

Voi*_*son 5

了解骨料的设计折衷很重要;由于聚合将您的域模型划分为独立的空间,因此您可以同时修改模型的不相关部分。但是,您在变更点失去了执行跨越多个聚合的业务规则的能力。

这意味着您需要对这两件事的业务价值有清晰的了解。对于不会经常更改的实体,您的企业可能宁愿严格执行而不是并行更改。在数据经常更改的地方,您可能最终会更倾向于隔离。

实际上,隔离意味着评估企业是否有能力减轻“冲突”编辑使模型处于不令人满意状态的情况。

使用此汇总,我们可以更改用户的类型,但只能是可用于该帐户的类型(不变式之一)。

对于这样的不变量,要问的一个重要问题是“这里一次失败的商业成本是多少”?

如果UserAccount是单独的聚合,那么您将面临一个问题,即在一个帐户取消对该类型的支持的同时,将用户分配给“类型”。那么(更改后)检测到发生了“不变式”的违背,您将花费什么?应用更正会花费什么?

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

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

我认为您已经失去了这里的情节。“域模型”并不是真正的命名事物,它只是集合的集合。

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

Account : Aggregate {
    accountId : Id<Account>,
    name : AccountName,
    users : List<User>,
    usertypes : List<UserType>
}
Run Code Online (Sandbox Code Playgroud)

这种设计意味着需要通过“帐户”汇总访问对用户的所有更改,并且没有一个用户属于一个以上的帐户,并且其他任何汇总都不能直接引用该用户(您需要直接与“帐户”汇总进行协商) 。

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

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

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

仅将Account和User之间的关系存在于Account聚合中,以及将用户的类型(作为AccountUser实体)存在,将其余的用户信息保留在单独的User聚合中怎么办?

该模型可以解决某些问题-在这种情况下,帐户汇总可能看起来像

Account : Aggregate {
    accountId : Id<Account>,
    name : AccountName,
    users : Map<Id<User>,UserType>
    usertypes : List<UserType>
}
Run Code Online (Sandbox Code Playgroud)

如果某人当前试图从某个帐户中删除某个用户类型,则此设计允许您引发异常。但是,例如,它不能确保此处描述的用户类型实际上与独立用户集合的状态保持一致-或确定所标识的用户存在(如果您遇到这种情况,您将依靠检测和缓解) 。

那个更好吗?更差?如果没有更彻底地了解要解决的实际问题,就不可能说(试图从玩具问题中了解确实很困难)。

原则是要了解必须始终保持哪个业务不变性(与以后可以进行对帐的情况相对),然后将必须保持一致的所有状态组合在一起,以满足不变性。

但是,如果帐户可以拥有数百或数千个用户怎么办?您对总体的看法是什么?

假设约束相同:我们有一些汇总来负责允许的用户类型范围....如果汇总太大而无法以合理的方式进行管理,并且业务所施加的约束无法放松,那么我可能会折衷“存储库”抽象,并允许设置的验证规则的执行泄漏到数据库本身中。

DDD从最初的OO最佳实践根源得出的想法是,该模型是真实的,而持久性存储只是一个环境细节。但是,在一个流程具有生命周期并且存在竞争的消费者的世界中,以务实的眼光来看待……持久性存储代表着业务的真相。