业务逻辑的拆解其实就是团队提高业务理解达成共识的过程,为了让这个过程更加高效和可复制,其拆解方法和拆解结果需要通过某种形式固化下来。DDD里的领域模型就是一种很好的表达形式,DDD里还提出了多种领域模型来适配不同粒度的业务逻辑。因此领域模型就是业务逻辑的抽象。

有哪些领域模型?

DDD里能承载业务逻辑的领域模型有:实体、值对象、聚合(特殊的一种实体)、领域服务、应用服务、领域事件。

概念

DDD定义

举例

实体

实体对应业务对象,具有业务属性和业务行为

线索是个实体,线索的状态随着线索跟进动作随时变化,根据唯一标识来追踪变化

值对象

值对象是相关性很强的多个属性的集合,对实体的状态、特征进行进行描述

线索上的联系方式信息是值对象,不需要唯一标识去追踪联系方式的变化过程

聚合

聚合是由业务和逻辑紧密关联的实体和值对象组成,是数据修改和持久化的基本单元

线索是个聚合,线索实体是该聚合的根实体,状态信息、联系方式信息等是聚合的附属

领域服务

领域服务没有任何属性或数据,只是一个领域行为或动作,不适合放在任何聚合的领域行为

线索的查重行为属于领域服务,线索聚合只负责单个线索的数据和行为,没法完成查重行为

应用服务

应用服务大致对应一个用户用例,通过编排聚合、资源库、领域事件、外部适配接口、领域服务

线索创建用户用例对应线索创建应用服务,服务内会编排多个步骤

领域事件

有重要业务含义的事件,事件发生后通常会触发后续的业务操作,或者引起系统外的处理步骤

线索创建后会产生线索已创建领域事件,后续的线索分配服务、打标签服务将监听该事件启动后续操作

02

为什么要领域建模?

为什么要领域建模?领域模型对业务逻辑的分析和提炼是必需的吗?如果逻辑足够复杂那么建模是很有必要的。前面讲到自我介绍其实也是一种建模。

  • 要上幼儿园的小孩为了让老师了解自己时只要说叫什么名字,几岁了就可以,可能需要凑点爸爸妈妈的信息来丰富内容。由于其自身经历少,要介绍的就是些非常基本的信息。

  • 要进入职场的新人为了让用人单位了解自己时需要按照简历的模板,详细写下个人基本信息、教育经历、获奖情况、实习经历、兴趣爱好。要介绍多个层面的信息。

  • 要结婚的人为了让对方了解自己时仅靠工作简历是远远不够了,还需要花很多的时间谈恋爱,互相熟悉对方。整个过程除了要『介绍』个人基本信息和职场信息,还会涉及家庭其他成员信息,财产信息,性格信息等等,可谓是360度无死角地做了一次自我介绍。

工作简历就是职场自我介绍的一种建模,用人单位是主导方和需求方,简历格式和内容都是按照用人单位希望看到的来准备,一提到工作大家就是它是为了解决什么问题,因此建模首先是围绕问题的。工作简历不是自传,其内容是需要提炼并结构化表达出来,工作简历格式不会经常变,因此建模是抽象提炼的过程,建模结果需要比较稳定和结构化。一提到工作简历大家就知道大概包含哪些内容,需要怎么写,因此模型是高效沟通的工具,能大大降低大家对同一个问题的心智理解,甚至能成为一个常识知识。

现实的软件开发流程中,产品经理眼里的业务逻辑大多是场景、流程、功能、规则。研发眼里的业务逻辑大多是:系统模块、类(对象)、类属性、方法,数据库表和字段。经常发现大家讨论了半天其实说的不是同一个东西。如何跨越这个鸿沟?如何能够让这两种思维模式不一样的群体能够更有效地协作完成业务系统的开发?这就是领域建模要解决的问题。领域建模就是为了让产品和研发能从问题出发,提炼业务逻辑并结构化表达出来,并成为日常沟通的基础,达到了模型(设计成果)和代码(实现成果)的绑定。

特定软件系统有很多『著名』的领域模型,这个领域里的人一听到它就知道要解决哪类问题,怎么解决的。

  1. 权限设计系统的RBAC模型,四个字母就概括了权限设计要解决的问题和设计方案包含的要素。

  2. 可观测系统里的Log、Trace和Metrics领域模型,非常经典的概括了如何表达系统的可观测性。

  3. 电商领域商品系统里的SKU、SPU模型,提到它就知道建模商品时需要考虑商品展示和售卖等不同维度。

  4. 云原生服务网格架构下的SideCar模型,概括了服务网格目前最通用的一种落地实现形式。

03

领域模型怎么做?

虽然领域建模是DDD的战术设计步骤,但其实从战略设计阶段的业务流程拆解就已经开始了,也就是说业务逻辑是领域建模的主要输入。另一方面业务逻辑和模型有不一样的地方。一则业务逻辑偏重流程、角色互动、和功能点,偏动态的行为。领域模型偏静态的数据,主要描述数据和数据关系。二则业务逻辑的拆解一般自顶向下,粒度从大到小。建模过程往往跟搭积木类似,把每个小零件做成小积木,再把小积木搭成大积木,是一种从小往上,粒度由小到到大。如何弥补这两者的距离?从业务逻辑梳理过渡到模型是建模方法主要考虑的地方。这也是传统的面向数据库表设计这类纯数据模型驱动设计方法的缺陷所在,无法有效衔接上业务逻辑拆解分析的结果。

3.1 领域建模方法和步骤介绍

领域建模方法大体有两大类,第一类就是用户用例建模法。第二类是事件建模法。传统软件开发流程里需求分析有用户用例,基于用户用例发现领域概念是自然而然的做法。DDD社区里更流行的是事件建模法,因为事件能更好弥补行为和数据的差异。一则事件有比较强的时间属性,能对应上实际业务流程往往带有明显时间节点性的行为。流程往往伴随开始、推进、流转、再推进、结束等状态行为变化。二则事件的触发和处理需要参考数据和规则,这些数据大多来自配置数据和过程中产生的业务数据。

3.1.1 用例建模法

用户用例往往用一个动词加一个名词的表述,因此也有叫作动名词法,其大致步骤是:

  • 整理用例,包含角色及角色要达到的目标。

  • 提取用例中的名词和概念作为『实体』的候选,讨论清楚实体的含义和范围。

  • 围绕实体建模,模型里包含需要达成业务目标的属性和方法。明确实体之间的联系。

  • 建模完成后形成团队的统一语言。

用例建模法比较简单,适合对业务比较熟悉,业务逻辑相对简单的场景。

3.1.2 事件风暴法

事件风暴法是DDD比较推行的,事件流很⼤程度上反映了现实业务流程,事件就跟讲故事一样,能够非常好地调动起现场人员的参与积极性。基于领域事件发生的时间线,能够把事件的前因后果逐步挖掘出来。事件风暴用类似记叙文六要素一样通过几个概念来清楚描述一件事:领域事件、命令、角色、决策规则、领域名词。

事件风暴的步骤:

  • 通过头脑风暴方式寻找出领域事件,注意不是所有的用例都对应着事件,比如页面查询展示场景就不是。

  • 根据事件找出触发它的角色,可能是用户,也可能是系统

  • 根据事件找出触发它的命令,可能来自角色,也可能来自系统比如定时任务,消息订阅等

  • 通过事件找出触发的规则和条件,命令触发后需要做出的策略,比如支付能成功必须有绑定好的支付账户

  • 通过事件找出事件发生后产生的结果即领域名词,往往是业务数据的新增和修改

  • 根据当前事件找出它的前因后果,是不是之前发生了什么前序事件才有了当前事件,当前事件发生后会引发什么后续业务

  • 重复上述步骤找出整个业务流程相关的事件流

举个例子:

最原始的需求往往就一两句,比如这个场景:销售员通过中间号电话拨打自动跟进一条线索。这个场景需要拆解出多个业务流程(线索创建、与运营商系统打通、系统内绑定中间号、线索跟进支持中间号,标签建设等等),每个流程涉及到多个功能点。

事件风暴建模法能把相关业务流程的功能点串联起来,并以事件的因果逻辑做到查漏补缺。因此事件风暴里需要打破沙锅问到底,不停地发问并回答,让所有参与的人都能够知道业务逻辑的来龙去脉。比如:

  • 拨打电话和线索状态改变是什么关系?这就反推出系统需要能够分析拨打电话的话单。通过判断电话的拨通状态,谈话内容等修改线索的状态。

  • 如何能让销售员第一时间跟进线索呢?这就要求在线索创建后系统能自动通知到销售员。

  • 该通知哪个销售员呢?这就需要设定规则进行线索的自动流转分配。

图片

发散之后如何收敛

事件风暴的过程往往会太过发散而收不拢,从而陷入分析瘫痪。因此事件风暴的成功很大程度上取决于如何收敛,这也要求主持人或者领域专家能够发挥重要作用。但另一方面可借鉴的收敛原则语焉不详,因此实际情况中大家上手容易,但用好的不多。这里也总结几条个人经验。

  1. 用户交互是一条时间线,但它不等同于领域事件流。是否为领域事件一般以是否改变了领域对象的状态作为判断依据。查询数据这类用户交互场景就不属于领域事件。

  2. 紧扣因果关系。领域事件流紧密联系着业务逻辑,通过像『查户口』的方式是把事件的因果关系进行追根溯源,以此来推敲业务逻辑的严密性。比如修改订单事件必然需要先查询到已创建好的订单。

  3. 紧扣事件要素(领域事件、命令、角色、决策规则、领域名词),不相关内容不要发散。

  4. 优先关注happy-path即正向路径,一是提高大家的熟练度,二是避免太过发散收不拢导致看不到分析结果。

  5. 每次分析的业务流程尽量在一个子领域内,最好在一个BC内展开。

  6. 事件风暴不是一蹴而就,需要保持迭代进化。

其他注意事项

  1. 命名:紧扣业务,不参杂技术元素,警惕使用泛泛的词汇,尽可能地消除命名的⼆义性。从已发生视角命名事件(名词+动词过去式),命令命名采用动词+名词。

  2. 要以可视化方式把事件风暴过程和结果记录下来,不同元素用不同的颜色区分表示。

  3. 不要太在意工具和形式。没有白板条件的时候退而求其次也可在线方式进行,使用任何画图软件都可以。

3.1.3 四色建模法

四色建模发展自Peter Coad的《彩色UML建模》,旨在把所有的模型对象抽象为四种,并在模型图里用四种不同的颜色标记出来。

  1. 时刻-时间段原型(Moment-Interval Archetype,MI):这类对象对应领域事件,用来记录某个时刻或某段时间内管理和运营数据,用粉色表示。比如订单系统里,订单就是典型的MI对象。

  2. 参与方-地点-物品原型(Part-Place-Thing Archetype,PPT):业务流程中的参与者,可以是人、物、地点。用绿色表示。

  3. 角色原型(Role Archetype):PPT对象在参与MI过程中,扮演的角色,用黄色表示。

  4. 描述原型(Description Archetype):对PPT对象的更详细描述,用蓝色表示。

四色建模步骤:

  1. 确定系统的关键业务流程,分解出关键步骤。

  2. 这些关键步骤是否需要可追溯数据,缺失了这些数据会不会影响运营管理?需要可追溯的数据正是MI对象。比如线索创建会产生线索记录,如果线索记录不可追溯,后续的销售跟进、线索转化怎么办?这就意味着线索数据必须作为MI对象存在系统里。

  3. 识别出上述MI对象涉及到的参与方、地点、物品。比如上述线索对象需要有线索的创建人、所在的线索池、所归属的销售员、线索联系人信息。这些信息就是PPT对象。

  4. 根据上一步识别出来的MI对象和PPT对象,找出它们之间的关系,抽取出来的角色就是Role对象。比如只有管理员才能分配线索给销售员,管理员属于线索池范畴,因此线索池就是线索的一个角色模型。

  5. PPT对象往往还需要更多的补充信息,比如联系人信息这个PPT对象还有地址、联系方式等补充信息,这些补充信息就是描述对象。

个人对四色建模法还不是很熟练,但它的底层思想很值得参考,即从企业运营出发,围绕息息相关的关键数据(可能是现金往来的记录,也可能是企业运营核心数据资产)展开业务流程分析。业务流程是为了满足业务系统的目标,业务系统的目标是要支撑企业的日常运营、商业决策。而运营管理过程中每个动作都会留下痕迹。

3.2 领域模型设计

前面建模方法得到的领域事件各要素需要通过不同的领域模型来承载。

3.2.1 业务数据视角建模:聚合、实体、值对象设计

建模方法得到的领域名词和规则,需要进一步定位为值对象、实体和聚合。但聚合是个虚拟概念,本质上还是实体,因此所有业务数据的载体要么是实体,要么是值对象。建议的设计优先级是先设计值对象,再设计实体,最后是聚合设计。

实体和值对象设计原则**:**

  1. 是否具备唯一标识是实体和对象的本质区别。实体一旦创建会有后续状态变化,每个变化都需要通过唯一标识来追踪。

  2. 什么时候该设计为值对象?有自我验证逻辑的属性(也叫字段),这些属性的验证逻辑如果放到实体里,会导致职责不够清晰,实体代码容易膨胀。比如邮箱属性它有自己的格式校验逻辑。有自我计算逻辑,比如金额属性包含数字和币种,币种的换算逻辑建议放到金额值对象里。其他情况不要设计为值对象,当做实体的普通属性。

  3. 值对象可以单独存储为一张表吗?可以,如果够简单就随实体表一起保存,如果复杂可以有单独的表存储。存储几张表跟领域模型数量不直接相关。

  4. 能够自给自足完成业务逻辑(计算或校验)的行为应该设计为实体或值对象的方法。

聚合设计原则

  1. 【独立性】只有聚合根才是访问聚合边界的唯一入口

  2. 【完整性】尽量保持聚合领域概念的完整性

  3. 【访问原则】聚合之间应通过聚合根的身份标识进行引用,聚合内部的实体间可以通过对象引用

  • 其他聚合需要加载一个聚合时(往往通过领域服务/应用服务协调),必须通过目标聚合的资源库,并返回聚合的完整内容,但资源库可以有支持多种加载条件的接口。

  • 新增一个聚合必须调用根实体必要的校验和领域逻辑,并传递给资源库一个完整聚合

  • 更新一个聚合必须调用根实体必要的校验和领域逻辑,并传递给资源库一个完整聚合。资源库的实现层面可以做到局部更新

  1. 【聚合协作】同一个BC的聚合间协作可以采用本地事务保持一致性,跨BC(即跨微服务)的聚合协作通过基于消息机制的最终一致性

  2. 【聚合与资源库】不要在聚合中访问资源库

  3. 【聚合粒度】在上述规则满足的前提下,聚合设计尽量小,小聚合带来小的事务粒度、有更好的性能。一开始可以把每个实体当作一个聚合,通过判断聚合是否足够完整和实体是否有独立性读和写的需求来合并实体,比如实体A和实体B生命周期一致,同时A没有被外界独立访问的需求,则合并A和B为一个聚合。如果实体A有被独立访问的需求,则A作为单独聚合。

3.2.2 业务行为视角建模:聚合、领域服务、应用服务设计

所有业务行为的载体要么是聚合(即实体或值对象),要么是领域服务,要么是应用服务。

领域服务和应用服务设计原则

  1. 不属于实体或值对象中的领域行为放到领域服务,比如线索创建时的查重功能,因为需要跟数据库的全部线索电话号码进行查重,这不属于线索聚合的行为。

  2. 看起来领域服务和应用服务都能做编排功能,原则上是应用服务不应该包含领域层的逻辑,那怎么判断什么是领域层的业务逻辑?这个问题没有标准答案,设计时不用太条条框框。一个简单的判断标准:这段逻辑是否与本BC的主要职责直接有关?如果相关的话则应放在领域服务。

  3. 领域服务和应用服务能编排资源库、聚合、适配接口。注意聚合里实体和值对象不是编排单元。

3.3 怎么衡量建模质量

首先建模很考验对业务的理解和设计水平,因此不太好衡量模型质量。如果模型建好后能够在团队内持续使用起来并能得到效率的提升那就是好的。下面仅列举一些现象或者味道来侧面反映建模的质量:

  1. 产研要共同参与。既增加了对模型的认同感,也形成了领域的统一语言。单方面建出来的模型容易腐烂不容易演进。

  2. 模型是统一语言的载体,是能传播起来的。模型里的元素如实体、规则等都需要有对应的统一语言描述,并在成为团队内部日常沟通的语言基础。模型成为了产研讨论需求的载体,研发讨论代码的依据。

  3. 模型降低了理解门槛。新需求的开发能够建立在良好基础上进行演进,不至于每次开发新功能都要重新学习业务逻辑和代码逻辑,研发通过代码就能轻松看懂系统的业务逻辑。

  4. 模型沉淀了团队的领域知识,团队成员有业务经验的增长,团队新人能够快速上手。

04

结语

本篇阐述了领域建模的定义、方法和作用。如何能够让软件开发团队内不同工种的人在面对复杂的业务时能够共同理解好业务并高效协作?领域模型是DDD给的一个答案。通过领域模型来分析和表达业务逻辑,这种模型思维是一种比较好的答案。领域模型让线上系统、日常交流、文档和代码这四者能够协同演进。后续讲述跟架构和代码有关的内容。