本指南的目的是通过工作坊的形式,让软件开发团队取得对业务的共识(统一语言),并输出能够落地使用的领域模型。为软件的编写和维护提供指导,帮助软件工程师设计出合理的架构。
DDD 软件建模工作坊(以下简称 “工作坊”)有多种形式,当前的版本是事件风暴,尚有其他形式的建模方法,例如:
事件风暴是一种 “自底向上” 的设计方法,先关注具体的业务细节,然后通过归纳、聚合、抽象的方法获得整体层面的认知和设计。
事件风暴的发明人是 Alberto Brandolini ,它来源于 Gamestorming,通过工作坊的方式将领域专家和技术专家召集到一起,进行共创建模。
事件风暴(Event Storming)是一种捕获行为需求的方法,类似传统软件开发的用例分析法。所有人员 (领域专家和技术专家) 对业务行为进行一次发散,并最终收敛达到业务的统一。
所以事件风暴可以作为 DDD 建模工作坊的一种重要的形式。使用事件风暴工作坊建模的逻辑闭环是:
工作坊的开展需要做一些准备工作,便于有序开展。
工作坊通俗来说就是相关人员在一起开会,通过一些互动的讨论的方式让每人个人都参与其中。工作坊可以有线下或线上两种形式。
如果是线下,需要准备一个大的会议室,能容纳足够多的人,并需要准备下列材料:
如果是在线上进行,需要准备必要的会议和协作软件:
本工作坊关注于软件架构和建模,需要合适的人员参与,这些人员需要接受过 DDD 和业务培训。
这些参与人员需要对 DDD 的基本概念有了解和认识,如果没有,需要参加 DDD 基础培训。这些概念包括但不限于如下:
工作坊开始前需要对齐业务输入,比如 PRD 文档、原型图、业务流程等资料,如果没有,需要领域专家做一次业务导入。
一些业务承载物参考:
明确本次的工作坊的任务目标,工作坊个常见目标有:
需要对齐工作坊的范围,明确工作坊各阶段的输出,例如:
产品愿景是企业对其主要的产品的定位,需要表达清楚产品的价值、服务群体、竞争力和主要的竞争对手。 通俗来说就是弄清楚产品背后的生意,然后通过软件来支持和实现。无论是否是传统软件公司或者互联网公司,产品愿景都是头等大事,否则做出来的软件找不到背后的生意,往往无法落地或不赚钱。
电梯演讲是一种思维工具,它提供了一套模板,用一句话把要做的产品说明白。其来源于著名咨询公司麦肯锡,在乘电梯的 30s 内需要清晰明白地向目标客户讲解清楚方案。
电梯演讲的格式并不重要,其结果无对错之说,一套简单的形式如下:
对于:我们的目标客户/用户
他们想:目标客户的痛点或者诉求
这个:产品名称
是一个:什么样的产品类型(软件、网站、工具或平台)
它可以:通过什么样的能力,为用户解决什么问题
不同于:市场上的竞品及其特点
它的优势是:我们产品聚焦的价值和竞争力
愿景:概括产品定位
在工作坊中,让领域专家处于核心位置,获取准确的定位,让其他角色做补充。实际操作中,可以在一个大白纸中使用便利贴书写,如果遇到错误可以方便的拿掉。
这里虚拟了一个业务背景作为这份指南的一个案例。我们在设计一个社区产品,这个产品具有圈子的属性,能让用户在圈子中提出和自己专业相关的问题。使用社区产品作为业务示例,是为了考虑到社区是大多数人都熟悉的业务场景,并且复杂性可控(可以做到很简单,也可以拓展的比较复杂)。
下面是我们的电梯演讲:
对于:需要找到自己专业问题和回答的用户
他们想:提出和自己专业知识相关的问题,以及找到人来回答
这个:滴答问题平台
是一个:问答平台
它可以:通过不同专业圈子,让用户能在特定范围内提出疑问,并获得回答
不同于:知乎、贴吧
它的优势是:结合圈子的模式,以问答的形式聚焦和归纳问题,留存用户
愿景:聚焦专业的问答社区
领域(Domain)是业务相关知识的集合。
子域(Sub Domain)是领域的一部分。子域的划分是为了,识别问题空间的关注重点,建模完成后用来验证解决方案。
核心域(Core Domain)是指领域中最核心的部分,通常对应企业的核心业务。
支持域(Support Domain)是一种特殊的子域,是指为了实现核心业务而不得不开发的业务所对应的相关知识的集合。
通用域(General Domain)是另一种特殊的子域,对应的是业界已经有成熟方案的业务。
场景(scene)是某种角色的用户,对系统的一系列操作,表现为一组领域事件。
根据这些场景,从最关键的业务出发,来做事件风暴,弄清楚场景中发生了什么,然后再进行业务建模,以便做出的软件模型,能很好的支持这些场景。
领域和限界上下文没有相互包含的关系。可以通过分析领域,导出限界上下文,限界上下文需要能支持领域。举个例子来说,某个电商网站有多个渠道,零售、批发、企业采购等多个场景的业务,这是他们的领域。对于研发工程师来说,他们会最终设计出订单、商品等模型上下文,来支持这些领域。
使用电梯演讲,得到产品的愿景和定位,这对业务非常重要。接下来我们需要分析出业务,业务场景是我们需要在软件设计时候满足的功能。
比如,滴答问题平台在建设的时候,考虑到主要的使用者是对某些行业问题有疑问的用户。于是提供了问答、圈子的场景,可以在某一个圈子提问,并寻找人来回答。
请注意,滴答问题平台为了演示整个流程,做了大量的简化。现实中,实际业务会比上面的例子复杂很多,比如专注于企业采购电商网站有企业核验、采购、结算、发票开具、简单进销存、维护工单等场景。
我们可以把这些场景划分为:核心域、支撑域和通用域。目的是结合产品定位,对业务做出重点分析。但是,实际上对领域的划分是一个模糊的概念,领域很难有边界,因此,重点是找出核心域,为后面的寻找事件工作提供输入。
可以通过文本描述或领域划分图表达领域的划分。场景往往互相交叉,不必在乎场景划分的长度和方式,尽可能足够长和覆盖足够多的场景,可以让模型更准确。
滴答问题平台的领域划分为:
事件(Event)是系统状态发生的某种客观现象。
领域事件(Domain Event)是和领域有关的事件,是在业务上真实发生的客观事实,这些事实对系统会产生关键影响,是观察业务系统变化的关键点。领域事件一般是领域专家关心的。
事件的评价方式是系统状态是否发生变化。系统状态变化意味着领域模型被业务规则操作,这是观察系统业务的好方法。
识别领域事件的线索有:
是否产生了某种数据
系统状态是否发生变化,无论这种状态存放到数据库还是内存
是否对外发送了某些消息
选定一个业务场景为单位,确定一个开始事件和一个结束事件,事件格式参考 “XXX 已 YYY”,比如 “用户已登录”。使用便利贴在物理墙面上张贴,或在电子白板中操作。
需要注意:
便利贴参考图例:
命令(Command)是执行者发起的操作,构成要件是执行者和行为。
命令可以类比于 UML 分析中的业务用例,是某个场景中领域事件的触发动作,在技术实现的时候,对应应用层中的一个方法。
执行者(Actor)是指使用系统的主体,是导致系统状态变化的触发源。
执行者有点像 UML 的涉众,不过区别是执行者不仅是用户,还包括外部系统和本系统。 在事件风暴中,执行者可以是:用户、外部系统、本系统、定时器。
用户(User) 是指使用软件或服务的人。用户可以有不同的角色,通常我们会把不同角色的相似行为作为不同的命令来处理,有可能得到同样的事件。比如系统出现了商品已添加的事件,有可能有多个触发的场景:
外部系统(Out System) 是开放 API 的调用发起者。
本系统(System) 指系统本身,事件的触发可以由用户、外部系统、定时器触发,也可以由上一个事件触发,因此这里的触发者(主体)就是系统本身。
定时器(Timer) 通常是定时任务,定时器可以作为执行者,不过需要区别于本系统这个触发源。定时器可以看待为外部一个时间信号源,类似于计算机中主机中的振荡器。
需要注意:
在操作时,在命令和事件之间保留一个便利贴的位置,为后面的流程腾出位置。
在这个阶段,我们尚不引入模型在技术实现上的细节,比如(聚合根、实体、值对象等),为了保证模型在业务上的简单性,先使用 “领域名词” 代替。
模型(Model)是对对象、人或系统的信息表示。它通过较为简单的信息结构来代表我们需要理解的复杂事物或系统。
领域模型(Domain Model)是业务概念在程序中的一种表达方式。
模型是用来设计和理解整个软件结构,切记不要建立事无巨细的模型。在模型思维中,模型是简单的,能反应业务概念即可。在事件风暴过程中,由于事件是关键点,可以体现出关键的业务模型所体现的业务逻辑变化。
他们的逻辑关系是:
用自然语言来概括就是:【执行者】发起了【命令】,产生了【事件】,导致【领域模型】的状态变化。
为了更好的编写代码还需要对生命周期以及业务规则强一致的模型做出区分。其现实意义之一就是避免在设计数据库时变成一张网,而应该退化成一棵树。因此需要设计聚合。
聚合(Aggregate)是一组生命周期以及业务规则强一致的实体和值对象的集合,表达统一的业务意义。 聚合的意义在于让业务统一、一致,在面向对象中有非常重要价值。聚合是在相同的上下文中,不能跨上下文。
实体(Entity)是在相同上下文中具有唯一标识的领域模型,可变化,通过标识判断同一性。 领域模型可以是一个广义的概念,建模的结果和中间过程其实都可以看做模型。
值对象(Value Object)是一种特殊的领域模型,值对象是以内容判断,不可变,同一性。 ID 标识,但是值对象是用属性标识,任何属性的变化都视为新的对象。比如一个银行账户,可以由 ID 唯一标识,币种和余额可以被修改但是还是同一个账户;交易单中的金额由币种和数值组成,无论修改哪一个属性,金额都不再是原来的金额。
聚合根(Aggregate Root)是聚合中最核心的实体,其他的实体和值对象都从属于这个实体。 要管理聚合必须使用一个聚合根,然后使用聚合根来实现发现、持久化聚合的作用,完成统一的业务意义。一个聚合中有且只有一个聚合根,聚合也可以只有一个单独的实体。
设计聚合,并定义出所有的属性。
参考图例:
注意事项:
限界上下文(Bounded context)是一个显式边界(边界:通常是一个子系统或者一个特定团队的工作),领域模型存在于边界之内。建立模型过程中形成了通用语言,通用语言在特定上下文中才有明确的意义。
限界上下文强调概念的一致性。DDD 不追求全局的一致性,而是将系统拆成多块,在相同的上下文中实现概念一致性。识别上下文可以从概念的二义性着手,比如商品的概念在物流、交易、支付含义完全不一样,但具有不同内涵和外延,实际上他们处在不同上下文。
限界上下文划分是应用 DDD 的一个难点,其本质就是对同类模型的划分,可以借助下列线索:
由于我们的示例比较简单,这里再补充一个复杂的示例。
工作坊期间通常使用便利贴表达内容,这些内容不适合最终落地使用。本阶段需要对白板的数据做整理,并形成规范化的输出。
通常来说 UML 是一种比较好的模型表达工具。如果考虑代码化的 UML 的工具,可以使用 PlantUML。
PlantUML 可以通过简单直观的语言来定义模型图,为了使用 UML 表达 DDD 中的概念,采用如下约定:
参考官网 https://plantuml.com/zh/class-diagram 或在 Intellij IDEA 中安装 plantuml 插件。
UML 描述语言:
@startuml
namespace user-context {
User <<Aggregate Root>>
VerifyCode <<Aggregate Root>>
Authorization <<Aggregate Root>>
}
namespace question-context {
Question <<Aggregate Root>>
Anwser <<Entity>>
Question "1" *-- "N" Anwser
}
namespace space-context {
Space <<Aggregate Root>>
SpaceMember <<Entity>>
Space "1" *-- "N" SpaceMember
SpaceApply <<Entity>>
Space "1" *-- "1" SpaceApply
}
@enduml
图形: