领域驱动设计实践01:捕获行为需求与领域建模

58 阅读10分钟
  • 最近在给团队内部做DDD实践落地的宣讲与培训,期间也略有一些心得, 会定期发表出来跟大家一起交流。如果文中有错误的部分,恳请大家批评指正。
  • 感谢 ThoughtWorks 各大DDD专家的不懈布道。

DDD(Domain-Driven Design)的定义:是一种开发复杂软件的方法与思想。

DDD中的领域,指的是软件要解决的业务问题,因此也可称为“业务领域”(Business Domain)。

DDD是以领域模型为核心的,具体分为2个阶段:

  • 领域建立(2个步骤):捕获行为需求(事件风暴) → 领域建模
    • 捕获行为需求:也就是传统意义上的获取需求,识别业务流程与功能、由哪些角色操作、产生什么结果等
    • 领域建模:通过建立领域模型,把主要的业务知识描述清楚
  • 领域实现(3个步骤):架构设计 → 数据库设计 → 代码实现
    • 实现的部分主要就是基于之前的领域建模,通过代码的方式呈现。

后续案例中主要基于一个小型企业服务项目展开,其中的业务功能主要包含:租户管理、人员与组织管理、项目管理、工时管理等。

一、捕获行为需求:事件风暴

为什么需要事件风暴:需求通常停留在领域专家的脑海中,很难一股脑全部讲出来,事件风暴正是一种套路化方法,能够把这些需求挖掘出来

捕获行为需求指的是:分析系统具有哪些功能,这些功能由什么人操作,会产生什么效果。

过程中最常用的方法就是事件风暴,其主要过程如下:

image.png

  • 识别领域事件:业务流程中发生了什么
  • 识别命令:什么角色做了什么操作
  • 识别领域名词:从领域事件、命令中找到概念性名词

1.1 第一步:识别领域事件

领域事件:指的是业务流程中每个步骤引发的结果。(注意,必须是引发的结果)

例如:订单(已)提交、商品(已)签收等。

从操作结果来梳理需求,比直接从操作过程中梳理,更容易梳理清楚。事件风暴中的“事件”,就来自于领域事件。

在命名领域事件时,通常使用的是【xx已xx】的形式。但如果已经有广泛使用或现成的行业术语,则应优先使用这部分术语。例如:通常用【已立项】而不是【项目已创建】

识别领域事件是团队协作等过程,尤其注意业务人员、开发任务要统一语言(统一业务术语、用词等)。

如下两个种情况,不属于领域事件:

  • 技术事件,例如:事务已回滚、缓存已命中等
  • 查询功能,因为查询并不能对事物产生实际影响

在梳理与识别领域事件时,可以按不同的业务流分别梳理到表格中。这是团队协作的过程,每个领域事件都要得到团队集体的认可。首先可以在表格中维护业务流程名、领域事件(业务结果),备注(约束条件):

image.png

1.2 第二步:识别命令

命令,指的是引发领域事件的操作。例如【合同已签订】这个事件对应的命令就是【签订合同】。

除了识别出命令本身,还要能识别出来如下信息:

  • 谁执行的命令
  • 执行这个命令查询了哪些数据

这时,我们就可以在表格中添加3列:命令、执行者、查询数据:

image.png

1.3 第三步:识别领域名词

领域名词,是从命令、领域事件、执行者、查询数据里找到的、受影响的名词性概念。例如:命令【签订合同】、领域事件【合同已签订】,影响到的名词性概念都是【合同】。

可以根据前一步骤汇总的数据(领域事件、命令、执行者、查询数据),把围绕同一个名词的放到同一个组下,如果一个命令或领域事件涉及了多个领域名词(例如:【员工已分配到项目】这个领域事件就同时与【员工】、【项目】这2个领域名词有关),则誊写多份,分别放到不同的领域名词分组下,并标记誊写的次数(2、3等)。

示例如下:

image.png

部分分组(企业、管理员等)缺少领域事件也没关系,可以在后面领域建模等时候再细化。

1.4 注意事项:

  • 在事件风暴里只列出主要的步骤,无需列举增删改等基础功能
  • 事件风暴中领域事件只需要按照大致的时间顺序呈现,不用很严格(如果需要严格时间顺序,可以单独画具体的流程图、时序图等)
  • 每个步骤(一对领域事件与命令)的粒度宜粗不宜细。例如“签订合同“是一个合理的步骤,不用拆分成”录入合同基本信息“、”录入合同明细“、”上传附件“等
  • 事件风暴只是理清业务等一种手段,实际执行时不用拘泥于形式。可以用表格等形式,以电子等形式保存、维护事件风暴的成果。
  • 领域规则(业务上必须成立的条件或约束)是重要的领域知识,也可以借助表格的形式梳理,与事件风暴(捕获行为需求)的成果分开维护,这部分的代码需要完全在领域层实现。

image.png

二、领域建模实践

领域建模的目的:

  • 准确反映领域知识,把知识可视化,在业务、技术人员之间达成一致
  • 指导系统与编码,能够较容易地转化为数据库模数与代码实现

建立领域模型,就是要:

  • 识别领域对象(domain object)

  • 识别领域对象之间的关系

  • 识别领域对象的关键属性

  • 按需把领域对象组织成模块

2.1 建模实践

领域对象指的是系统中要处理的各种“事物”,比如说项目、员工、账户等等。

领域模型图通常用UML图(Unified Modeling Language)来画,其中的领域模型常用类图来画,表头表示领域对象的名词,下面的属性表示对象的属性。

image.png

在领域建模过程中说领域对象时,有时指类,有时指实例,一般可以通过上下文来区分。比如,员工是领域对象的一个类,张三和李四是这个类的实例。

在领域建模阶段,在画类图的时候,无需写出属性。主要关注的是实体和它们之间的关系,如果实体的名字已经能清晰说明实体的含义,那就不需要加属性了。

如果只关注前面梳理的租户、组织,则可以先画出如下的类图

image.png

由于每个实体都要属于租户,因此,UML图上就不把租户跟每个实体关联起来了,而是在租户的旁边加上一段表示约束的说明:

image.png

现在我们开看下企业、开发中心、开发组,他们从左到右依次是一对多的关系。如果直接画到UML图中,则如果其中后续组织架构调整,要添加其他部门,或开发中心下不再设置开发组,那这个模型就要修改了。

也就是说,这个模型不易适应业务的变化。这个时候就要把实体进行抽象(归纳共性)

企业、开发中心、开发组,都可以抽象为“组织”,通过类型区分。并添加自我关联的方式体现上下级关系。

示例如下:

image.png 其中:

  • 每个类图都表示一个领域对象
  • 领域对象之间的连线表示关联关系
  • 领域对象的自我关联则体现了上下级的层级关系,线边的文字表示角色(例如:上级、下级……)
  • 带折角的图形描述了相关的说明,通过虚线连接到领域对象
  • 带折角图形中用{  } 包裹的部分,表示对应的约束,需要在代码中做对应的实现
  • 线边的数字表示A对象最少..最多关联多少个B对象

注意同步修改领域汇总表(事件风暴成果物)中的内容:

image.png 在建模过程中项目想把多重性搞清楚,则可以通过问四个问题:

  • 实体 A 最多可以对应多少个实体 B?

  • 实体 A 最少可以对应多少个实体 B?

  • 实体 B 最多可以对应多少个实体 A?

2.2 拆分多对多关联

有些业务数据是基于关联关系记录的,只有关联关系成立,这些业务数据才有意义。这个时候可以通过引入一个表示关联的实体,把一个多对多关系拆成两个一对多的关联。

比如员工与项目是多对多关系,这个时候又需要记录员工的预计投入百分比,这个属性记录在员工或项目都不合适。这个时候就可以引入一个表示关联的实体(项目成员),来记录这些需要记录在多对多关联上的属性

最终形成了如下的模型。

image.png

依赖与关联表达的是不同的含义。

  • 关联:表示的是数据上的导航关系。例如,当我们说组织和员工之间具有一对多关联的时候,就意味着,由组织可以找到下面的员工,由员工也可以找到所属的组织。
  • 依赖:表示的意思更为广泛。如果 A、B 两个元素,有了 A 才能有 B,那么就可以说 B 依赖于A。

此时,这个图已经有点乱了,很多实体和关联混杂在一起,而人的认知能力是有限的,面对这样一张复杂的对象网络,就产生了认知过载。需要把这张图模块化,把模型中的业务概念组织成若干高内聚的模块(module),而模块之间尽量低耦合。

2.3 划分模块

在 UML 中,可以用包来表示模块,包的内部可以包含实体,也可以包含另外的包。包的符号是下面这样:

image.png

经过模块化处理之后,把模型分成了 4 个模块,分别是租户管理、组织管理、项目管理和工时管理

image.png

有了模块,就可以从两个层面理解模型。

第一个是宏观层面。宏观层面只关心模型中有哪些模块,以及模块间的依赖关系,不关心模块的内部细节:

image.png

其中的虚线表示依赖关系,箭头由依赖方指向被依赖方。

第二个层面是微观层面,也就是深入到模块内部,了解实体和关联等等的细节。

为了完善开发过程,我们还要再进行两个实践:一个是完善业务规则,另一个是建立词汇表。

词汇表可以规范领域模型中的词汇,并用于后续编程中的命名

image.png

image.png