DDD(Domain-Driven Design)实战
1,系统架构
我们有如下(微服)系统架构,在项目初期,可能因为快速上线,资源有限等条件限制,系统很可能不会做到极致的细粒度划分。
图 1
以下我们核心围绕Manager部分来看看,怎样用DDD来设计分布式(微服务)应用架构。
2,DDD分布式应用架构
图 2
如上图所示,从相对高层的角度看,平台的领域模型设计图,大体上可划分为3部分:1)左边部分描述的Manager(设备管理)模块的领域驱动模型;2)右边部分分别是User、Resource、Control等基础功能模块;3)面向网关,暴露给app/web访问的Api模块,设计为修改/查询分离。特别地,我们就Manager模块的来分析设备管理相关业务的详细领域驱动模型设计。
模型分为4层:Domain层,Biz层(业务逻辑层),Data层(数据持久化管理层),Api层(Controller接口层)。从图2的模型图可以看出,其它所有层都只依赖Domain层。这就是DDD(Domain-Driven Design)的设计原则,以Domain内核为核心,各层可以独立于具体技术实现,聚焦于业务核心需求和规范,一旦业务定义成型,后续迭代应不去(或尽可能)不修改Domian Core,以达到快速响应上层业务变化需求。在图1(微服务架构图)可以看到,整个大的Manager模块划分为几个小服务模块:Share(共享模块)、Device(设备模块)和Group(分组模块)。每一个微服务模块又遵照各自的领域来驱动设计,同样又可以独立划分为:Domain层、Biz层、Data层和Api层,并独立部署。
然而,前期考虑业务量有限,从节省资源以及系统的稳定性和维护成本上看,图1中Manager模块的各个微服务,很可能共用同一个持久化存储/缓存服务,或同一个关系型数据库,这就完全省去了考虑分布式数据一致性问题。
此外,从图2也可以看到,ShareService和DeviceService,DeviceService和GroupService之间存在紧密的依赖关系,合并部署并且共享同一个数据中心,可以有效地避免分布式部署引起的数据不一致问题。最后,我们仍然会考虑到后续系统拓展性的需求,因为随着业务的发展,无论是业务功能的增加,还是业务量的增长,都需要系统可以弹性地支持扩展。而系统以DDD的模式设计,正是能解决这样的需求。因为我们前期已经系统内核层就已划分好Domian边界,后续若要拆分领域实现或增加系统吞吐能力,并不会引起领域内核、接口或数据存储的变动。
接下来,我们给出详细Share(共享模块)、Device(设备模块)和Group(分组模块)的详细领域驱动设计模型。
- Share领域模型
图 3
- Group领域模型
图 4
在图5中描述了设备和分组的关系(DeviceGroup)。在本方案中,首先应该明确设备的分组关系是属于分组管理领域边界内的。既然是属于分组领域边界内的,那么将此关系移到Device领域,将隐藏以下问题:
1)在对设备进行分组管理时,会在Data层出现Group和Device之间的领域耦合,势必导致后续领域无法拆分,扩展困难。例如,任何对设备分组关系的操作(GroupRepository#modifyDeviceGroup),都涉及对Device表的操作,导致底层数据层无法相互隔离,后续拆分困难。
2)当需要通过groupId来查询设备信息时,是让Group领域提供接口,还是让Device领域提供?显然,该关系在那个领域,就由谁提供是最方便。而把关系移到Device领域后,让这些变得混乱不堪。
最后,显然在Group领域定义关系(DeviceGroup),不仅易于系统扩展,也支持在基础基础平台提供一样非通用(但应用很广泛)的功能的同时,也能方便地拓展业务规则。
- Device领域模型
图 5
下面再看看,在具体工程里,如何实现DDD的分层应用架构。
3,DDD分层应用架构
图 6
如第2部分描述或图2所示,DDD应用系统分为Domain层、Biz层、Data层和Api层。明确地说,只要是遵照DDD模式设计的任何一个微服务(模块)系统,都可分为这4层。
接下来,我们在细致的看看图2的应用架构,涉及的Java Import和Maven依赖是如何体现的。
4,Java Import依赖
图 7
可以看到工程有以下特点:
- 所有层的import依赖只有Domain层
- Biz层对(持久化或缓存)数据的操作具体实现一无所知
- Api层对数据的检索或业务逻辑的具体实现一无所知,只遵照接口规范暴露结果
总之,是以Domain为核心,隔离具体实现,达到真正意义上各层之间的解耦。
5,Maven依赖
图 8
这里我们应该会有疑问:DDD不是叫领域驱动设计吗,为什么Api、Biz和Data层的maven依赖不仅依赖Domain,还相互之间依赖?这里一定要区别软件解耦设计和Maven打包之间没有任何关联。以接口来解释解耦,就是接口,约定了一项协议,隔离了客户端和服务端,使得客户端不需要关心服务端的具体实现。我们所说的软件设计,本质是解耦设计。
下面几点帮助理清maven依赖和Java import依赖的区别。
DDD开架构思想是围绕领域核心来开发,所有层之间的代码依赖都只存在于对domain的依赖。具体的:
- 在集中式开发模式下,虽然api层的代码只需要依赖domain层的代码。由于domain只定义了业务接口,并没有提供实现,要打包成一个可执行的jar包,又必须有实现来才行。但如果把biz层的实现类依赖进api层的类中,又会破坏DDD的设计原则。此时,IoC正是解决了我们的烦恼,使我们只要真正实现,只依赖接口,就可以利用IoC帮助我们解耦具体实现逻辑。
- 当我们的业务量扩大,需要发展成分布式开发模式了。原来api层依赖的domain层的业务接口,变成了facade接口(任意rpc形式调用)。此时,maven也不再需要依赖biz层的实现类,代替的是rpc的底层技术实现。
6,一个困惑
在实际项目实战时,我们会遇到一个不可避免地困惑,查询业务依赖Service or Repository?答案是Repository。下面举个栗子说明:
例如“查询组下的设备列表”, 在api层直接调用device和group领域的repository合并数据,而不是通过在biz层合并。
1,当在集中式条件下,这种做法可以避免冗余代码;
2,即便在后续要以分布式环境开发,domain是相互之间完全独立不可见的,模块之间的依赖只有facade(feign)。此时,只需要api模块依赖device和group模块的facade,而不是让group模块依赖device的facade,又让api模块又再次依赖group模块facade。
7,一个查询接口代码示例(查询组内设备列表)
图 9
8,一个修改接口代码示例(设备解绑)
图 10