DDD已经火了很久,目前在很多项目上都有所应用,而这次是我第一次参加DDD相关的培训,对我来说神秘的DDD一层一层揭开了它的面纱。
背景
DDD为什么这么火,它终归是解决了软件设计与开发过程中的一些痛点的。
培训过程中有一句话“任何人类的设计都会随着时间腐化,软件系统也不例外”,可见在软件开发过程中,一开始做的软件设计,随着时间的流逝、需求的变更,慢慢变得不再合理,反而成为了负担。
那么在软件系统整个开发过程存在着的挑战有哪些呢?
- 在软件开发中存在着不确定性,而这个不确定性会一直贯穿软件工程的生命周期中。
- 没有所谓的“银弹”,能够用来解决软件的复杂度问题。
- 在软件工程里沟通非常重要,如何让整个团队都保持着信任和良好的沟通就成了要思考的问题。
了解了背景和痛点以后再来看DDD可以为我们带来什么改变,让它成为了“潮流”。
先来了解一下DDD是什么
Domain-driven design,领域驱动设计,它是一种处理高度复杂域的设计思想。主要是围绕业务概念来构建领域模型,从而来控制住业务的复杂性,使得系统不断演进时仍然可以保持敏捷。
在传统的方法里,业务架构和系统架构是绑定在一起的,所以当我们去响应业务变化调整业务架构时,系统架构也会随之改变。
而对于DDD来说,主要聚焦在领域和领域逻辑,所以这种设计方式下的系统架构是向领域模型靠拢的,领域模型内部封装了数据和⾏为的对象,所以当业务发生变化时,不会影响整个系统架构。那么是由谁来识别领域从而做出设计的呢?是技术专家和领域专家一起合作,缺一不可,这样才能不断切⼊问题领域的核心。
那么在做领域驱动设计的时候有哪些原则呢?下面就来介绍一下。
DDD三个原则
上面我们说过领域驱动设计主要的焦点是领域和领域逻辑,所以第一个原则就是
1.聚焦核心领域
我们要先了解什么是领域(domain),看下面的图表可以知道领域中分为核心领域和子域,而子域又分为⽀撑⼦域和通用子域。
图表中具体的一个业务领域或子域其实指的是一个业务范围以及在业务范围内进⾏的活动。一个业务领域或子域可以包括多个业务能⼒,⼀个业务能力一般对应着⼀个服务。
那么图表中的几类领域有什么区别呢?
- 核⼼子域一般是指业务成功的主要促成因素,是项目的核心竞争⼒;
- 通⽤⼦域虽然不是核心,但被整个业务系统所使用;
- ⽀撑⼦域虽然也不是核心,也不被整个系统使用,但是是完成业务的必要能力。
我之前老是分不清通用子域和支撑子域,因为核心子域很好区分,但是这两种子域都是“辅助”型的,就傻傻分不清楚了。后来想想顾名思义,通用子域,重点在于通用,如果这个子域被很多个业务系统调用,它就是通用子域。如果一个子域单纯的只是作为某一个业务领域的“附属”,没有通用的定义,它就是支撑子域。而一个支撑子域随着业务需求的发展,也有可能摇身一变成为通用子域。
2.通过协作迭代式探索模型
在传统模式中,是基于所有未来的假设需求而做的庞大的设计。例如,基于造车的需求,做好一个庞大的设计后,就会开始先造轮子、再造车架子、一步一步往车的模型上走。
而迭代式探索模型是根据需求的变化来不断演进模型。在迭代式探索模型中,同样是造车的需求,就会先造一个滑板车、自行车、摩托车、慢慢演进成车。在实际场景中,其实很难一开始就订好整个项目的所有需求,一般都是有个长期的目标或者明确的蓝图,然后对于近一个月的需求比较明确,对于近2周的需求非常明确。所以真实实践过程中,迭代探索模型比较适合敏捷开发流程。
3.使用统一语言
在介绍DDD的时候说过,领域驱动的设计是由技术专家和领域专家一起合作完成的。所以通过良好的沟通协作,统一语言就很重要。
对于技术人员和业务人员来说,他们各自有着自己的语言,例如技术人员大部分表达的是技术术语、技术设计模式、关于技术方面的设计,而业务人员大部分表达的是业务术语、业务需求,所以需要技术人员和业务人员一起来统一语言,避免分歧。而这个被统一后的语言就是领域模型术语。
了解DDD的三个原则以后,再来看看如何在实施过程中遵循这三个原则。
DDD的实施步骤
1.团队消化业务知识,建议统一语言。
一开始就是技术专家和领域专家一起协作,双方都明确的了解业务知识,在整个讨论过程中建立一个统一的领域模型术语,在之后设计模型时使用。
2.初步提炼领域模型,识别领域模型。
技术专家和领域专家把领域模型术语都明确出来以后,识别出哪些术语是一个领域的,每一个领域内部承载的职责,初步提炼出领域模型。
3.分解领域模型复杂度,划分子域和上下文。
在初步提炼出领域模型后,需要明确的知道每个领域之间的关系、交互等等,从而来划分出子域和上下文。
4.细分领域模型内部元素,识别实体值对象和聚合根。
前三个步骤都是在一层一层往上抽象,第四步回归到领域内部,识别出领域内的实体对象和聚合根有哪些,这个就涉及到比较细节的业务功能。
由上面4个步骤,我们知道如何去实施DDD,但是却不知道,每一步的细节是如何做到的,例如怎么去找出领域模型术语?怎么确认哪些术语是一个领域?怎么归纳子域和上下文等等。这个时候就可以考虑用到事件风暴工作坊(Event Storming)了,具体的实施方式请看下文。
Event Storming
Event Storming是⼀种领域建模的实践,是⼀种快速探索复杂业务领域的⽅法。
Event Storming最开始是由Alberto Brandolini开发的一种方式。在2015年11月进入了ThoughtWorks技术雷达,这也意味着Event Storming这种方式被大家所认可,有着它的实际价值。
- Powerful: Event Storming可以让实践者在数⼩时内理解复杂的业务模型,是一种快速探索复杂业务的很好的方式。
- Engaging: Event Storming的参与人员就是带着问题的人和拥有答案的人,他们共聚一堂来构建模型。
- Efficient: 它跟DDD的实现模型⾼度一致,并能快速发现聚合和 Bounded Context [1]
- Easy: 在 Event Storming 中的标记都很简单,没有复杂的UML
- Fun:这整个过程非常的愉快,不会让人觉得枯燥乏味。
[1] Bounded Context在DDD是分而治之的意思,就是不要把一整个很大的系统看成一个context,而是依据「某种方式」把整个大系统分成若干个有界限的context。
了解 Event Storming 以后,主要是了解怎么进行 Event Storming,看看怎么使用这种方式来构建领域模型。
1.确定参与人员
业务⼈员,领域专家,技术人员,架构师,测试⼈员等关键⻆色。
2.确定产品愿景和价值定位
产品愿景是对产品的顶层价值设计,主要是对产品的⽬标⽤户、核⼼价值、差异化竞争点、痛点等策略层面的信息在团队内部达成共识。
可以套用模板:
对于 "目标用户群体"
它们想 "诉求或痛点"
这个 “产品名称”
是一个 “产品特征”
它可以“不可抗拒的优点”
不同于 “其他竞品”
我们的产品 “核心的差异化竞争力”
最后得到的结果是:
3.事件风暴
事件风暴顾名思义就是对于“事件”的头脑风暴,用来梳理业务流程、建立领域模型、划分边界。
3.1设计好业务场景
事件风暴第一件事就是设计好要讨论的业务场景。根据之前定下的产品愿景与价值定位,设计出关键场景,找出业务的起点与终点。
因为往往整个产品的功能是很复杂的,包含了很多业务场景,但是在做事件风暴时我们主要聚焦在关键的业务场景,然后找出业务起点和业务终点,确定业务范围。例如在我们这个例子中原本是一个玩具网上商城,但是它的核心竞争力是玩具置换,所以我们设计的关键场景就是玩具置换的流程,业务的起点是发布玩具,终点是玩具置换完成。
3.2识别领域事件
识别出业务范围内发生的事件,一个领域事件指的是业务上真实发生的事情,必须对业务有价值,有助于形成完整的闭环。
事件都是有时间顺序的。列举事件可以用“名词+动词”这样的形式,例如“玩具已发布”。
3.3事件排序
将上面识别出来的事件贴在白板上,并且每个事件从左到右按时间顺序排列,同时间段发生的要保证相对顺序。例如下图是整理后的玩具置换业务模块的事件图。
4.命令风暴
命令指的是产⽣事件的领域⾏为或领域活动,命令一般由三种方式触发:(1)用户从UI界⾯进行的操作 (2)外部系统触发(3)定时任务
4.1识别命令
命令风暴这个过程中,需要识别出命令有哪些,把它贴在事件的左边,如果有命令产生了多个事件,就用包含箭头的虚线连接起来。如下图中的蓝色小方块就是命令。
在上图中“确认意向”这个命令会触发“意向单已确认”、“置换单已创建”、“玩具已下架”等三个事件,所以用包含箭头的蓝色虚线连接起来。
4. 2识别出触发命令的⻆色
找出命令对应触发它的角色并标识出来。如下图中的黄色小方块,就是触发这个命令的角色。例如玩具置换发起方会发起一个意向,然后意向单就会被创建。
5.寻找领域模型
在前面的步骤中,已经分析出领域事件,主要是根据事件来寻找领域模型。在上面事件的模板都是“名词+动词”的方式,所以很容易识别出事件中有业务含义的名词,需要确保事件中的名词代表的业务概念清晰完整并且没有歧义。
上面的事件例子归纳总结后,发现领域模型只有“玩具”、“意向单”、“置换单”、“物流单”。
6.寻找聚合
聚合是一组相关领域模型的集合,是为了保证边界内领域对象的业务不变性。
它通过定义对象之间清晰的所属关系和边界来确保关联关系紧密的领域模型能够内聚在一起,从⽽避免错综复杂的对象关系网形成。聚合内部的领域对象是具有⼀致的⽣命周期的。
如何来寻找聚合?是通过前⾯的领域模型来确定的:
- 领域模型是否可以被独⽴访问,如果可以被独⽴访问就是一个聚合。
- 如果领域模型不能被独⽴访问,就应该属于它依赖的聚合
下图就是整理出来的聚合,不巧,刚好每一个领域模型都是一个聚合,然后把命令放在聚合的左面,事件放在聚合的右面。
7.提炼子域
子域的概念,我们在解释DDD的部分的时候说过,简单来说⼦域就指的是一个业务范围以及在业务范围内进⾏的活动。
我们需要回顾产品愿景与价值定位,找出用户痛点和产品的核心价值。然后根据前⼀步识别的领域模型图来讨论子域,并确定映射关系,最后验证我们的⽅案是否解决并覆盖全部问题空间。
例如在我们的例子中:核⼼子域就是置换子域,支撑域是商品子域,通用域是物流子域。关于支撑域和通用域也在DDD部分解释过,一个子域的定位完全根据你制定的业务需求来。在这里就只假设物流系统是共用的。
8.划分限界上下文
限界上下文可以分为限界和上下文两个词来理解,限界指⼀个界限,具体的某⼀个范围;上下文指的是场景、环境,所以限界上下文是在某个场景或环境下的业务边界。
随着业务的扩展渐渐的会产生越来越多的领域模型,任何一个比较大型的项目都会存在着很多的领域模型。如果把很多个领域模型相对应的软件代码全部放在一起,避免不了存在着很多有歧义的对象,使得代码难于理解,找不到真正该改动的代码导致 bug,所以需要通过限界上下文来明确定义领域模型的范围和职责,来消除歧义。
要知道如何去划分限界上下文,就需要知道划分的规则:
术语相同,含义不同
在一个系统中,可能一个术语有着多重含义,这个时候就需要将领域模型分拆在多个界限上下文中,以避免歧义。一个上下文中的术语应该只有一种含义。
例如:术语“账号”在不同的限界上下文中,含义是不同的。在上下文A中,账号跟是和外部系统A对应的账号;在上下文B中,它是这个系统本身的账号,与外部系统A只有部分信息是一致的。
概念相同,用法不同
另一种情况就是虽然底层的概念是相同的,但是使用的⽅式不同,最终导致了不同的模型。
例如例子中的“玩具”,在整个系统中,物理上都对应着同一个玩具,共享了SKU数据。但置换上下文不能修改玩具的详情,但其实在置换上下⽂中也不该关心玩具的具体详细信息(玩具的图片、破损情况、原本购买价格等等)。所以“玩具”虽然物理上是一个玩具,但是存在于不同的上下文中。
外部系统
一个系统常常避免不了与外界系统对接,为了避免内部系统受外部系统的影响所以需要分离上下文。
例如:玩具的具体物流情况是与一个物流系统对接的。如果以前是用的物流系统A,但是后面对接了物流系统B,那么就出现了不同的外部物流模型。这个时候内部的物流模型会受这些外部物流模型的影响,为了避免这种情况就要分离上下文,保持内部物流模型不受影响。
以上就是Event storming得到的一些结果,会发现最后得出的内容是向DDD靠拢的。那么在通过Event storming进行领域建模之后,如何根据构造好的模型来拆微服务呢?
微服务拆分原则
在这里对于微服务的内容不做过多的描述,只是单纯的阐述如何根据领域模型来拆分微服务。
对于微服务的拆分原则,业界已有的共识:
1.【⾸选】依据业务限界上下⽂边界拆分。这种方式拆分的微服务在颗粒度上⽐较适合刚开始做服务化架构的团队,而且自然的在业务层面实现了高内聚[2]和低耦合[3]。
2.对于技术能⼒⽐较成熟的团队,可以依据聚合关系拆分服务。这种方式就将事物实时强⼀致性的要求,控制在了聚合范围内,服务间则通过最终一致性保证事物的一致。通过聚合来拆服务使得服务的颗粒度更更⼩,拆出的服务也更多。但是这种方式对于开发和运维团队的能力要求更⾼。
3.实际中也有一些别的服务拆分原则。例如按照变更的频率拆分、按照⾮非功能需求拆分、按照组织结构拆分等等,这里就不详细描述了,这些都是根据实际情况具体分析比较好。
[2] 高内聚:就是把相关的⾏为聚集在⼀起,把不相关的行为放在别处。如果你要修改某个服务
的行为,最好只在⼀处修改。
[3] 低耦合:如果做到了服务之间的松耦合,那么修改⼀个服务就不需要修改另⼀个服务。⼀个松耦合的服务应该尽可能少地知道与之协作的那些服务的信息。
总结
可以看出我们通过Event Storming的方式来构建DDD模型。通过构建好的模型,我们可以根据模型中的上下文或者聚合来拆微服务,这是实际场景中的应用。当然不是说这个模型只有拆微服务一个好处,它也能够体现在软件分层架构上,让实际的代码和模型相匹配,真正落地到项目实践中,只是文中没有这个部分的描述而已。