「尺有所短,寸有所长;不忘初心,方得始终。」
在DDD落地实践中分为【「战略设计阶段」】与【「战术设计阶段」】,而在这两个阶段中涉及到了很多的名词
「战略设计阶段」
在此阶段除了领域划分为子域、核心域、通用域、支撑域之外,还包含以下等名词:
「限界上下文」
「上下文映射图」
「事件风暴」
「战术设计阶段」
在此阶段是DDD从技术层面的落地实践,包含以下等名词:
「实体」 「值对象」 「聚合」 「领域事件」 「资源库」 「工厂」 「领域服务」 「应用服务」
一、通用语言
在我们程序开发过程中会涉及到两个角色:开发人员与领域专家,角色决定了认知,对于开发人员脑子想都是类,方法,设计模式,算法,继承,封装,多态,如何面向对象等,而领域专家是不懂这些的,在双方沟通过程中就产生误差,最终会导致程序开发的偏差。
「通用语言就是开发人员和领域专家的沟通桥梁,使得在建立领域模型,两者交换、描述知识范围及领域模型的各个元素的时候能够无障碍沟通。」
统一语言如何形成,对DDD没有标准的定义,可以是图,也可以是文字,UML类图是常见的表达方式。
战略设计、战术设计和技术实现都是基于统一语言环境下开展的。
二、限界上下文
1.1 限界上下文是什么
限界上下文(Bounded Context)可以分为「限界和上下文」两个词来理解
「限界」:一个界限,具体的某一个范围 「上下文」:特定环境下的语境
「限界上下文是一个显式的边界,主要用来封装通用语言和领域对象。领域模型存在于这个边界之内。在边界内的通用语言有特定的含义,而模型需要准确地反映通用语言。」
限界上下文定义了每个子域的应用范围,在每个上下文中确保领域模型的一致性。不同的限界上下文中,领域模型可以不用保证一致性。 通常我们根据团队的组织、软件系统的每个部分的用法及物理表现(如组件划分,数据库模式)来设置模型的边界。 「限界上下文根据业务单一职责和高内聚原则,定义了聚合内部应该包含哪些实体和值对象。」
1.2 限界上下文的命名
「限界上下文只是一个统一的命名,在划分子域后,每个子域一般对应一个上下文,也可以对应多个上下文。」
但如果子域对应多个上下文的时候,就要考虑一下是不是子域能否继续划分。
「命名方式:领域名+上下文。」
比如我们的销售子域对应销售上下文,物流子域对应物流上下文。
1.3 总结
子域和限界上下文保持一对一的关系。
识别限界上下文将那些不属于其中的概念放在另一个上下文中,再在通过上下文映射图标明两个限界上下文之间的关系。
三、上下文映射图
「多个系统之间存在交互,需要在各自的限界上下文上有所表现。上下文图(Context Map)便是表示各个系统之间关系的总体视图。」
在Context Map中可以有如下几种形式来表征限界上下文之间的关系:
「共享内核(Shared Kernel)」
「共享内核是业务领域中公共的部分,同时也是团队间容易达成且必须达成共识的领域部分。」
当不同团队共同开发应用程序时,团队之间需要进行协调,通常将两个团队共享的子集剥离出来形成共享内核,双方进行持续集成。
「客户/供应商(Customer/Supplier)」
上下游关系,由上游完成模型的构建和开发,并交付给下游系统使用。
「追随者(Conformist)」
上下游关系,「上游团队无法提供下游所需要的东西」。此时客户/供应商就不生效了,下游系统只能去追随上游系统,下游系统严格遵从上游系统的模型,简化集成。
「防腐层(Anticorrupttion Layer)」
「即ACL,某些上下游关系无法顺利实施,该层作为上游系统的代理向你的系统提供服务。」
如果上游的模型不适合下游的场景,下游系统又必须依赖于这些模型,此时需要使用防腐层模式将上游系统的影响降低。
「开放主机服务(open host service)」
即OHS,定义一种协议,允许系统将一组service公开出去公其他系统访问,在互通模型的同时,减少了系统间的耦合。
微服务架构可以理解为此模式的实现形式。
「发布语言(published language)」
即PL,「发布一种共享语言完成集成交流,通常和开放主机服务一起使用」。
「各行其道(Separate Way)」
当两个系统之间的关系并非必不可少时,声明两个上下文,两者完全可以彼此独立,各自独立建模,独立发展,互不影响。
四、事件风暴
在DDD的落地实践中我们需要去分析两个问题:
如何发现系统中的聚合 (Aggregate), 如何划分限界上下文 (Bounded Context)
为解决以上两个问题,DDD引入了事件风暴的概念。
「事件风暴(Event Storming)是一种轻量级的系统分析方法,基于 DDD 的概念,能够梳理系统中的各种相关元素」。在官方网站中有四个词解释:
4.1 事件风暴能做什么
在其官网已经总结了事件风暴的作用
「EventStorming」是一种灵活的研讨会形式,用于协作探索复杂的业务领域。它有不同的风格,可以在不同的场景中使用:
评估现有业务线的健康状况并发现最有效的改进领域; 探索新的创业商业模式的可行性; 设想新的服务,最大限度地为所有相关方带来积极成果; 设计干净且可维护的事件驱动软件,以支持快速发展的业务。
EventStorming 的自适应特性允许具有不同背景的利益相关者之间进行复杂的跨学科对话,从而提供超越孤岛和专业界限的新型协作。
4.2 事件风暴流程
4.2.1 物料准备
物料一般使用电脑就可以,不用电脑最好是准备一些不同颜色的便利贴
4.2.2 参与人员
「组织者」:组织者应当熟悉事件风暴的整个流程,能够组织大家顺利完成事件风暴;
「领域专家」:领域专家应该是精通业务的人,在事件风暴过程中,要负责澄清一些业务上的概念,思考业务上有没有遗漏的事件;
「项目成员」:负责开发这个项目的成员,所有角色都可参加,包括系统开发人员,业务分析师,业务人员,测试工程师,UX 设计师,项目管理人员等。
4.2.3 识别领域事件
「事件风暴将系统拆分为不同的元素,用不同颜色表示」
领域专家和团队成员通过头脑风暴把领域事件(业务行为)都梳理出来,例如:电商的领域事件集,我们表述是:「主语+定语:订单已创建。」
4.2.4 分析命令和事件
在梳理完领域事件后,我们可以在此基础上进一步探索系统核心事件的运行机制。这里我们在之前的领域事件的基础上加入事件,命令和角色的概念。
「事件(Event)」
「事件风暴中的核心概念,是领域专家关心的,在业务上真实发生的「业务行为」,描述的形似为宾语+动词的过去式」。
例如: 「订单已提交」,「账户已锁定」,「商品已发出」。使用「橙色」表示。
「命令(Command)」
「产生事件的对象。命令可以理解为是一个动作,执行了动作之后就会产生相应的事件。」
例如:「取消订单」。使用「深蓝色」的即时贴表示。
「角色(User)」
执行命令的对象,一般是指自然人。
4.2.5 分析领域模型和聚合
「领域模型」:「相同概念指令和事件的集合」,一般用黄色表示。
领域模型相关的命令放到左边,事件放到右边。
「聚合」
当某一个领域模型不能作为一个独立存在的对象。它被另一个领域模型持有和使用。此时我们将两个模型结合起来形成一个聚合。
4.2.6 划分子域和限界上下文
「当确定领域模型以后,就可以划分子域和限界上下文」。子域划分在【领域划分】会有详细说明。
在划分限界上下文的时候可以「检验领域模型和通用语言的正确性」。
五、实体&值对象
5.1 实体(Entity)
「DDD中的实体是拥有唯一标识符,经历各种状态变更后仍然可以保持不变的对象。」
实体可以被多次修改,一个实体对象可能和它先前的状态大不相同。由于它们拥有相同的身份标识,他们依然是同一个实体。比如人会经历幼年,少年,中年,老年几个状态,但是始终都是这个人没变。
这里就有了唯一标识符是这个人,并且在多个状态都还是这个人体现了连续性。
实体有两个重要特性,它们可以超出软件的生命周期:
标识(identity) 连续性(continuity)
「对与实体而言重要的是标识与连续性,而非实体的属性」。
在DDD不同的设计过程中,实体的展现形式不一样
5.2 值对象(ValueObject)
「通过对象的属性值来识别的对象,它将多个相关属性组合为一个概念整体,是没有标识符的对象。值对象本质就是一个集合」。
比如:我今天去买包子,包子铺有很多包子,我买了一个包子,这个包子就是一个值对象,我不会去关注它是怎么做出来,什么时候做出来的,对于我而言只要它是一个包子就可以了。
值对象描述了领域中的「一个不可变的东西」,它将不同的相关属性组合成了一个概念整体,「当度量和描述改变的时候可以用另外一个值对象替换,并可以进行相等性比较」。
作用:
「领域建模过程中,值对象可以保证属性的清晰和概念的完整性」。
在这个图中,当我们的领域对象是人员的时候,我们关注的是人员本身,而地址是人员的一个属性,我们把地址构成一个集合,即地址值对象。
六、聚合
6.1 聚合根
在理解集合之前,先来理解一下什么是聚合根
「聚合根:如果把聚合比作组织,聚合根则是组织的负责人,聚合根也叫做根实体,它是实体并且还是实体的管理者」。
「聚合根的特点」:
作为实体,具备自己的业务属性,业务行为,业务逻辑 作为聚合的管理者: 在聚合内部:负责协调实体和值对象完成共同的业务逻辑 在聚合之间:聚合根是聚合对外的接口人,以聚合根ID的方式接受外部请求和任务,实现上下文中的聚合之间的业务协同。
「聚合之间通过聚合根关联引用,如果需要访问其他聚合的实体,先访问聚合根,再定位到聚合内部的实体;外部对象不能直接访问聚合内的实体」。
领域模型根据领域分成多个聚合,每个聚合都有一个实体作为「聚合根」。 聚合确定了实体生命周期的关注范围,即当某个实体被创建时,同时需要创建以其所在的整个聚合。而当持久化某个实体时,同样也需要持久化整个聚合。即:「CRUD操作都应该作用在聚合根上」,而不是单独的某个实体。
6.2 聚合(Aggregate)
6.2.1 聚合是什么
在DDD中,实体和值对象都是很基础的领域对象,那么聚合就是它们的集合
「让实体和值对象协同工作的组织就是聚合,用来确保这些领域对象在实现公共的业务逻辑的时候,可以保持数据的一致性」。
「聚合是数据修改和持久化的基本单元,一个聚合中必然有一个聚合根,而一个聚合根对应一个数据的持久化」。
聚合的组成:
限界上下文 聚合根
6.2.2 聚合的作用
「聚合在DDD分层架构中属于领域层,领域层包含了多个聚合,共同实现核心业务逻辑,聚合内的实体以充血模型实现个体业务能力,以及业务逻辑的高内聚」。
跨多个实体的业务逻辑通过「领域服务」来实现,跨多个聚合的业务逻辑通过「应用服务」来实现;
「领域服务:业务场景需要一个聚合中的A实体和B实体共同完成」 「应用服务:业务逻辑需要聚合C和聚合D共同完成」
6.2.3 聚合如何设计
以下是一张保单投保的聚合过程(网图),从下图中可以很清晰的看出来聚合的设计步骤,以及方式方法
6.2.4 聚合设计原则
「设计小聚合」
聚合设计过大,聚合会因为包含过多实体,实体间管理复杂,高频操作时会出现并发冲突或数据库锁,导致系统可用性降低。
设计小聚合降低实体间复杂度,复用性高,领域模型更能适应业务变化。
「在一致性边界内建模真正的不变条件(高内聚)」
不变条件是一个业务规则,应该保持一致性:
「聚合封装的是不变的领域对象,而非简单地组合对象。内部的实体和值对象按照固定的规则实现数据的一致性,边界外的任何东西都与该聚合无关。」
事务一致性 最终一致性 「通过唯一标识符引用其它聚合」
「聚合之间通过聚合根的唯一ID来关联,而不是直接对象引用的方式」。
外部的聚合对象不能在该聚合内管理,容易导致边界不清晰,增加聚合之间的耦合度;
「边界之外使用最终一致性」
「聚合内部数据强一致性,聚合之间数据最终一致性」
「在一次事务中,最多只能更改一个聚合的状态」
如果一次业务操作涉及多个聚合状态的更改,应采用领域事件的方式异步修改相关的聚合,实现聚合间解耦。
「通过应用层实现跨聚合的服务调用」
在不持有对象引用的情况下,不能修改其他聚合,所以要避免在同一个事务中修改多个聚合。「但在领域模型中我们总需要对象之间的关联关系来完成一些任务。「此时就需要用到」通过应用层实现跨聚合的服务调用,也就是应用服务的服务编排」。
「通过应用服务的服务编排实现微服务内聚合之间的解耦,以及以聚合为单位的服务组合和拆分,应避免跨聚合的领域服务调用和跨聚合的数据库表关联。」
6.3 特点比较
「聚合」
「高内聚、低耦合,是领域模型中最底层的边界,可作为拆分微服务的最小单位」。
聚合可独立作为一个微服务,以满足版本的高频发布和弹性伸缩要求。一个微服务也可包含多个聚合。
「聚合根」
聚合根是实体,有实体的特点,具有全局唯一标识,有独立的生命周期。 一个聚合只有一个聚合根。 聚合根在聚合内对实体和值对象采用直接对象引用的方式进行组织和协调。 聚合根与聚合根之间通过ID关联的方式实现聚合之间的协同。 「实体」
有ID标识,ID在聚合内唯一。
状态可变,依附于聚合根,其生命周期由聚合根管理。
实体可以持久化,但与数据库持久化对象不一定是一对一的关系。
实体可引用聚合内的聚合根、实体和值对象。
「值对象」
无ID,不可变,无生命周期,用完则销毁。
值对象之间通过属性值判断相等性。
本质是值,是一组概念完整的属性组成的集合,用于描述实体的状态和特征。
值对象尽量只引用值对象。
七、资源库(Repository)
「资源库用于保存和获取聚合对象」。领域对象不需通过基础设施得到领域中对其他对象的引用。只需从资源库中获取。
资源库会保存对某些对象的引用。当一个对象被创建出来后,可以保存到资源库中,使用时可从资源库中检索到。如果客户程序从资源库中请求一个对象,而资源库中并没有它,就会从存储介质中获取它。「资源库作为一个全局的可访问对象的存储而存在」。
资源库与DAO的区别:
DAO只是对数据库的一层很薄的封装,而资源库则更加具有领域特征。 实体都可以有相应的DAO,但只有聚合对象才有相应的资源库。
Repository以「领域」为中心,把ORM框架与领域模型隔离,对外隐藏封装了数据访问机制。
public interface UserRepository {
User findAccount(String id);
void addUser(User user);
}
资源库保存聚合对象的时序图为:
八、工厂
「DDD中工厂的主要目标:隐藏对象的复杂创建逻辑,清晰的表达对象实例化的意图。」
工厂模式是计模式中的创建类模式之一。我们可以借助「工厂模式实现DDD中领域对象的创建」。
「工厂的设计要点」
每个创建对象的方法都应该是原子的,并保证生成的对象处于一致的状态。
「可以使用独立的工厂或者在聚合根上使用工厂方法」。
当 A 对象的创建主要使用了 B 对象的数据或者规则时,那么可以在 B 对象上创建一个工厂方法来生成 A 对象。
以下情况只需使用构造函数即可。
类仅仅是一种类型,没有其他子类,没有实现多态性。 客户端关心的是实现类。 客户端可以访问对象的所有属性,因此向客户公开的构造函数中没有嵌套的对象创建。 公共构造函数必须遵守与工厂相同的规则,必须是原子操作且满足所有固定规则。 不要在构造函数中调用其他构造函数,应保持构造函数的简单。 工厂方法的参数应该是较低层的对象。
九、领域服务&应用服务
「服务是行为的抽象」。根据DDD的分层架构,「应用服务属于应用层,领域服务属于领域层」。
DDD的四层架构如下,具体分层后续说明
刚刚接触DDD的时候最容易迷惑应用服务和领域服务。因为一般来说代码只会有一个服务层,什么逻辑都往里面放,没有什么应用服务和领域服务之分。
「应用服务是用来表述应用行为,而领域服务用来表述领域行为」。
应用行为描述了一个具体操作从开始到结束的每一个环节。
领域行为是对应用行为的细化,用来处理具体的某一个环节。
9.1 应用服务
应用服务是用来「表达用例和用户故事(User Story)的主要方式」。
应用层通过应用服务接口来暴露系统的全部功能。
应用服务主要负责「编排和转发」
将要实现的功能委托给一个或多个领域对象来实现,它本身只负责处理业务用例的执行顺序以及结果的拼装。「从而隐藏了领域层的复杂性及其内部实现机制」。
应用层在四层架构中是比较【薄】的一层。
除了服务编排,在该层还可以进行安全认证,权限校验,持久化事务控制,外部系统访问等。
应用层是防腐层与领域层的桥梁。
防腐层使用VO(视图模型)进行界面展示, 防腐层与应用层通过DTO(数据传输对象)进行数据交互,使得防腐层与领域层Entity(领域对象)解耦的。
「小结:」
「应用服务的职责」
跨限界上下文业务逻辑。DTO转换。事务AOP、权限AOP、日志AOP、异常AOP。外部系统访问:邮件、消息队列。
「应用服务的设计原则」
用来封装业务逻辑。面向用例和用户故事,一个请求对应一个方法。应用服务之间互不依赖。
9.2 领域服务
「领域服务是用来协调领域对象完成某个操作,用来处理业务逻辑的,它本身是一个无状态的行为。状态由领域对象(具有状态和行为)保存」。
当领域中的某个操作过程或转换过程不是实体或值对象的职责时,我们便应该将该操作放在一个单独的接口中,即领域服务。
「领域层是在四层架构中是比较【充实】的一层。,它实现了全部业务逻辑(业务流程、业务策略、业务规则、完整性约束等)并且通过各种校验手段保证业务正确性」。
「小结:」
「领域服务的职责」
处理聚合实例业务逻辑。没办法合理放到实体中的其它业务逻辑。
「领域服务的设计原则」
组织业务逻辑。面向业务逻辑,一个请求对应多个服务的多个方法,领域服务之间会存在依赖。
9.3 总结
当应用服务中的逻辑过于复杂时,我们应该重新考虑领域服务的划分,避免领域逻辑泄露到应用服务中去。而在使用领域服务时,因为有些操作更适合放到领域对象(实体和值对象)中去,这可能会导致贫血领域模型。
服务是行为的抽象。 应用服务「通过服务编排来委托领域对象和领域服务来表达用例和用户故事」。 领域对象(实体和值对象)负责单一操作。 领域服务用于协调多个领域对象共同完成某个业务操作。 应用服务不处理业务逻辑,领域服务处理业务逻辑。