TWInsights在之前的文章《??当我们谈论DDD时我们在谈论什么??》中我们讨论了DDD的战略设计和战术设计。在这篇文章中我们继续探索领域模型。使用领域模型表达领域概念在实际项目中,模型设计者往往过早地陷入实体、聚合、领域服务等特定构建块类型的识别,而忽略了领域模型表达领域概念的目的。我们应该基于领域概念来设计领域模型,然后使用合适的模式来降低领域模型的复杂度,进一步增加领域模型的表达能力。领域模型的作用是一方面关联代码实现,一方面关联通用语言。我们熟悉模型和实现之间的关联,但是语言和模型之间的关联往往需要改进。在我们的交流中有意使用通用语言可以帮助我们验证模型的可靠性。下面以一个话题为例,方便后续讨论。活动平台为用户提供参与活动获得奖品的功能,吸引用户和潜在用户参与,从而达到拉新用户、推广活动、吸引流量的目的。运营商可以创建和修改活动,活动的配置内容包括活动名称、活动介绍、活动开启的起止时间、参与资格、福利等。用户可以看到活动列表,在活动开启的时间段进入活动页面可以看到活动介绍。用户在活动页面申领权益,被判定符合条件的用户将获得奖品。好处可以是信用卡积分或优惠券。参与资格可以是:一天内注册的用户、VIP用户、生日在当月的用户等。客户希望系统能够方便的扩展支持灵活的资格类型,以支持各种形式的活动。对于一个事件,一个用户只能参与一次。建立模型的第一步是根据需求分析模型。我们可以找到以下概念:Activity、ParticipationEligibility、Benefit。参与资格是扩展点。对于“一个用户只能参加一次活动”的需求,需要记录用户是否参加过活动,因此需要“活动参与记录”的概念。参与活动可能有两种结果:满足参与资格,返还收益,不满足参与资格,返回“不符合资格”。所以我们使用了Optional类型。我们这里只识别了各种名词,需要通过用例来寻找缺失的概念:用例1,运营商可以创建和修改活动用例2,用户可以参与活动并获得收益对于用例1,创??建和修改activities,目前该模型已经满足需求。对于用例2,模型外有一条规则:“一个用户只能参加一个事件”。这些是所有竞选活动都需要遵循的规则,我们称之为“竞选活动的通用规则”。虽然只是一个很简单的逻辑,但是对于提炼出“活动总则”的概念,还是很有用的。如果没有这个概念,每次描述这个概念时,都只能用“一个用户只能参加一次活动的规则”来表达,非常繁琐;也让概念无处安身,很容易被随便放在万能的Service中。我们将它添加到域模型中。PS:这里特意省略了参与资格的实现。我们没有将“活动的一般规则”放入活动的概念中,部分原因是这种判断逻辑不需要有关具体活动的信息。使用通用语言验证模型对于域模型,有一种通用语言。用通俗的语言重新陈述要求,并在交流中尽量使用通俗的语言。运营商可以创建和修改活动,活动的配置内容包括活动名称、活动介绍、活动开启的起止时间段、参与资格、权益等。用户可以看到活动列表,在活动开启的时间段进入活动页面可以看到活动介绍。用户在活动页面领取福利,被判定有资格参与的用户将获得奖品。好处可以是信用卡积分或优惠券。参与资格可以是:一天内注册的用户、VIP用户、生日在当月的用户等。客户希望系统能够方便的扩展支持灵活的资格类型,以支持各种形式的活动。同时,还有《活动总规则》:一个活动,一个用户只能参加一次。此处删除“开始时间”、“资格”、“奖品”等含糊不清的描述。使用基于领域模型的语言使需求描述清晰明确。至此,主要的领域模型已经分析完毕。所有模型都对应于明确的领域概念,仅此而已。识别构建块类型在分析了域模型之后,我们来分析构建块类型。我们通过是否有状态来区分。首先识别有状态对象:活动、各类参与资格、权益、活动参与记录、用户。通常,有状态对象是事物,对应的构建块类型是实体或值对象。其次判断它的状态是否会改变:activity会被修改,所以状态会改变;参与资格会被修改,但参与资格从属于活动。修改后可以直接用新对象替换旧对象,这样就可以设计成状态不变;权益也可以像参与资格一样,设计成状态不变;活动参与记录,状态可能发生变化;用户在这个模型中只是暂时存在,状态不会改变。是改变状态的实体,包括活动和活动参与记录;不改变状态的价值对象包括参与资格、权益和用户。最后剩下的就是无状态对象:活跃的公共规则。相应的构建块类型是领域服务。这里的大部分无状态对象都可以转化为有状态对象。比如活动的通用规则可以把方法参数的Optional<活动参与记录>改成成员变量。只不过我们这里选择了无状态的设计方式。由于领域服务没有状态,它可以在应用程序启动时创建,也可以在使用时创建。经过分析,我们的领域模型是有类型的。设计聚合首先识别具有长生命周期的领域对象:在一个操作中创建并且在操作结束后仍将被其他操作使用的对象。活动、参与资格、福利、活动参与记录等都是生命周期较长的对象。所有其他有状态对象都是临时对象:在操作期间创建并且在操作完成后不再使用。模型中的用户,一次操作从其他服务中获取,使用后丢弃。这里我们总结一下每种积木类型的特点:实体-值对象领域服务是否有状态,状态是可变的,状态是不可变的,生命周期可长可短。在生命周期长的对象中,我们要设计聚合。聚合作为一个运算单元,主要解决以下问题:整个模型往往庞大而复杂。为了减少知识负荷,需要分解成多个小而简单的模型。边界清晰的模型对象之间存在一致性规则。比如需要一起删除,就需要放在一个操作中。多个用户可以同时操作模型。为了避免相互干扰,必须使运算单元尽可能小。对于运算单元,需要经常加载到内存中。如果单位过大Large,往往无法满足绩效要求根据对业务的理解,活动、参与资格、权益一起创建和修改,可以放在一个聚合体中;活动与活动参与记录之间没有一致性规则,可以分开;因为活动参与记录的数量会很大,如果和活动在一个聚合中,性能会降低。因此,我们将活动、参与资格、福利设计为一个聚合体,将活动参与记录设计为一个单独的聚合体。活动和活动参与记录分别作为这两个聚合的聚合根。相应的,聚合也会配备自己的Repository。还要添加遍历方向箭头。由于活动是聚合的根,因此可以遍历活动到聚合内的参与资格和收益。此外,您可以通过其Repository查询活动参与记录,因此没有从活动到活动参与记录的箭头。由于我们将活动和活动参与记录拆分到不同的聚合中,因此它们之间的关联将使用聚合ID关联,而不是聚合本身。PS:如果使用关联对象,遍历方向也可以从活动到活动参与记录。如何使用领域模型既然已经建立了领域模型,那么让我们看看如何使用领域模型来满足用例。运营商创建活动的基本信息及其相关的参与资格和权利。领域模型的客户端(一般来说是一个应用服务)使用操作者输入的参数构造一个活动对象,然后使用Repository保存。操作员修改活动。应用服务使用Repository获取需要修改的activity,然后根据operator提供的参数修改activity,最后使用Repository保存activity对象。用户参与活动。应用服务:使用事件的通用规则来确定用户是否可以参与。由于活动的一般规则需要使用活动参与记录,所以应用服务会使用Repository来获取活动参与记录;如果可以参与,则执行活动参与方法获取结果。这就需要使用Repository获取用户参与的活动,构造用户对象(可能需要调用用户服务获取用户信息,但是领域层不关心这些逻辑);如果结果是获得权益,则创建活动参与记录并使用Repository保存。考虑到并发,应用服务可以在step1之前加锁,step3之后释放锁。再想想(1)configuration和engagement可以是两种模型吗?在实现运营人员配置活动用例的过程中,我们可能会发现一个隐藏的领域概念。将输入参数转换为领域模型的逻辑有些枯燥和复杂,还要在领域模型和数据库的数据模型之间进行转换。同样的道理。输入参数和数据模型都只是没有继承结构的平面数据。也许这些用例使用另一个面向数据的模型来实现会简单得多。每个模型都是为解决特定问题而设计的。运营人员配置和用户参与活动在这里是不同的问题,可能很难用一种模式来解决这两个问题。那么简单地设计两个模型,并使用有界上下文的概念将这两个模型限制在各自的上下文中可能更合理。两个模型可以共享相同的数据库数据,并添加一段(非领域层)逻辑用于模型之间的转换。这实际上是一种配置使用模式。配置阶段,重点关注配置类型和参数、审批等;在使用阶段,关注逻辑计算和性能。(2)活动参与记录能否建模为领域事件?活动参与记录实际上是不可变的,可以设计为领域事件。(3)用户参与活动用例,逻辑复杂,有泄露领域概念嫌疑?如果你发现应用服务中的逻辑变复杂了,那可能意味着我们发现了一个隐藏的领域概念。我们可以定义一个“用户参与活动逻辑”的概念:如果用户通过了活动总规则的判断,则可以参与活动。将其合并到模型和通用语言中以验证通信中的概念。综上所述,虽然很多项目也采用以领域模型为中心的架构,但设计者仍然以数据模型/贫血领域模型的方式思考,将大量的领域逻辑放在无所不能的Service中,让领域概念隐藏在冗长的程序代码,你无法享受DDD带来的好处。最后总结一下本文要强调的要点:领域模型和领域概念与领域模型和实现关联一一对应,也与通用语言相关联。故意使用通用语言进行交流,以验证模型是否合理演示设计领域模型的一个步骤构建块类型不是最重要的,领域模型本身更重要更多使用值对象和临时值对象聚合可以表达商业意义这种设计需要方法的平衡。使用Repository和Factory获取和创建领域模型是应用层的职责。领域层应该专注于表达领域概念。