DDD 战术设计落地重难点——分包,分层,依赖关系,持久化设计

DDD 战术设计落地重难点——分包,分层,依赖关系,持久化设计

  • 前言
  • 项目架构
  • 组织结构
    • 层级视图
    • 数据视图
  • 难点
    • 查操作性能优化
        • 问题
        • 解决方案
          • CQRS
          • 延迟加载(这是我采用的方案)
    • 写操作性能优化
  • 结语

前言

关于DDD战略架构(划分子域,创建聚合模型等)在此不再讨论,这里探讨一下在DDD的战术设计上的一些重点和难点。
我的项目地址

我想很多人在看完DDD的设计原则后曾经自己也想过采用DDD去构建一个系统,但是在构建过程中却无从下手,不知道DDD的层级是怎样的,不知道它的各层之间的依赖关系等等,下面一一说明。

项目架构

准确来说,我的项目之前是三层架构,而现在将转换成DDD架构,那么我要做哪些?
DDD中最核心的层:领域层,而领域层是通过应用层来进行协调的,还有基础层,接口层,那么我们可以简单的这么类比:
DDD 战术设计落地重难点——分包,分层,依赖关系,持久化设计_第1张图片
图片引用自:中台架构与实现:基于DDD和微服务 (欧创新 邓頓)

那么我的项目架构为:
接口层(其实就是熟知的controller,以及在其中的入参合法校验)
——>应用层(用于服务间事件发布和处理,DTO解析和创建等)
——>领域层(核心层,这里是领域对象的业务逻辑)
——>基础层(包括持久层,第三方工具,CLI,SMS等在内的一些业务无关的基础设施)

那么我将其分包为:
分包

组织结构

接下来再来看项目的组织结构:

层级视图

对于系统的各个层级,他们各担当了项目中的什么角色,实现了什么功能?
DDD 战术设计落地重难点——分包,分层,依赖关系,持久化设计_第2张图片
图片引用自:中台架构与实现:基于DDD和微服务 (欧创新 邓頓)

  • 基础网关层作为整个服务的入口网关,对外统一提供服务;
  • 用户接口层使用facade(实现对入参的合法性校验)和封装结果对象;
  • 应用层作为服务间通讯(消息,RPC)的最低粒度和用于调度领域层对象;
  • 领域层包含了领域服务和具体的实体对象的行为,而涉及到需要使用仓储接口的则使用领域服务来实现(例如查询和保存);
  • 基础层中提供了仓储的具体实现,当然也包括一些其他的第三方服务。

数据视图

DDD 战术设计落地重难点——分包,分层,依赖关系,持久化设计_第3张图片
图片引用自:中台架构与实现:基于DDD和微服务 (欧创新 邓頓)

显而易见我们系统中包含了DTO,DO,PO,注意:VO是前端页面对象,我们系统中最外层对象是DTO

  • DTO:数据传输对象
  • DO:领域实体对象
  • PO:持久化对象

另外有一点是有争议的:关于DO转DTO到底是应该在应用层还是应该在用户接口层?我的想法是,应用层作为系统内服务间的通讯的最小粒度,那么我就该在应用层转换,同样的,DTO转DO也应该在应用层转换。其实只要你说的有道理,都是可行的,只要你符合高内聚,低耦合原则。

难点

查操作性能优化

问题

在使用了DDD设计原则之后,我们就迎来了一个问题:对于持久层操作,一个聚合是最小粒度,这就带来一个问题:

  • 如果我要查询某个聚合中的某个对象或是某个属性,我就必须要查询整个聚合。

为什么会有这个问题?举个例子,我们现在有一个订单聚合

订单聚合{
     
	订单信息{
     下单时间,付款总额},
	购买人信息{
     购买人姓名},
	订单详情{
     包含的商品}
}

我现在只要查询该订单的订单详情,但是我不得不查询出整个订单聚合,然后根据订单聚合来得到订单详情,这样在持久层来说(我可能采用了关系型数据库),可能为了查询出整个聚合我要进行多次表关联,查询出很多实际上无意义的数据,这就带来了性能上的消耗。

解决方案

CQRS

这个是网上较多的解决方案,通过Command(持久层写)与Query(持久层读)分离的思想,而如果有Query的需求,我可以直接跳过聚合对象,直接查询我需要的某些对象或是某些属性(映射到数据库就是某些字段)。

  • 优点:
    • 这种方案比较好接受,理解起来难度较低,毕竟Query跟我们传统的service是相同的;
  • 缺点:
    • 这个方案其实在一定程度上违背了DDD的原则,如果你将Command和Query分离,则他不是高内聚的(对于同一个业务分了模块),如果你将Command和Query合到一起,那么你就必须在Query中注入仓储对象,即仓储对象依赖了Query(违背了禁止跨级向内依赖);
    • 从实现上来说这个方案其实是比较复杂的,需要实现类似读和写的分离(当然你可以使用同一个数据源);
    • 为了实现CQRS,你必须设计一套同步事件机制,因此你的系统将不能保证强一致性。
延迟加载(这是我采用的方案)

先看一下我们的数据流:
DDD 战术设计落地重难点——分包,分层,依赖关系,持久化设计_第4张图片
我们从dao查询到PO之后,再将其转化成DO,期间可能会去对DO的一些属性做修改操作,最后再由DO转化成DTO,传输给前端页面。

那么我们举个例子,同样是订单聚合对象:

订单聚合{
     
	订单信息{
     下单时间,付款总额},
	购买人信息{
     购买人姓名},
	订单详情{
     包含的商品}
}

我们的写法应该是从dao查询到PO(如果是关系型数据库可能使用到表关联,这是主要的性能消耗),然后可能对DO做出处理(也就是说会执行某个或某些属性的getter),最后转换成DTO对象传输(一定会执行某个或某些属性的getter),所以说,我们实际需要用到的属性其实就是这些被调用了getter的属性。那么方案就来了,我可不可以在知道真正需要哪些属性(即数据库字段)后,我再去查询数据库呢?

完全是可以的!就拿上面的例子来说,我要在api层查询订单详情中包含的商品,我可以在dao层调用数据库查询时伪加载,即PO的属性都为初值,再将其转换成DO对象,在service层调用DO或是api层租状DTO时直接或间接使用到PO的某些属性的时候,再去数据库查询相应的字段,就可以大大减小数据库的性能消耗。

  • 实现
    具体的实现我采用了数据库查询框架的延迟加载功能,同时将PO的所有属性都设置为需要延迟加载,并且使用cglib代理实现了延迟加载的传递,将PO的延迟加载传递到DO中去(即是生成一个代理后的DO对象,并拦截其getter方法并对其做增强)。
    DDD 大粒度读与写性能优化

  • 优点

    • 这种方案对于局部的查业务能够通过减少表关联次数大大减小查询数据库的性能消耗(当然,只有我们持久化数据库选择了关系型数据库才有这个问题,如果我们选择mongo来作为持久化数据库,上面这些都是空谈!)
  • 缺点

    • 这种方案是延迟到真正需要直接或间接的使用PO的属性的时候再去加载数据库,而每次加载都是一次数据库连接,因此如果频繁的使用PO的某些字段而不显示的声明要关闭延迟加载机制,这将导致多次发送数据库连接并加载PO中的一些属性,须知:多次建立数据库连接的性能消耗要远远大于表关联!

写操作性能优化

说完了读我们再来看看写,下面讲一个场景,同样是订单聚合:

订单聚合{
     
	订单信息{
     下单时间,付款总额},
	购买人信息{
     购买人姓名},
	订单详情{
     包含的商品}
}

如果我现在要修改订单详情中某个商品的数量,在遵循DDD规范的前提下,我应该是这样的:

  • 查询整个订单聚合
  • 修改订单聚合中订单详情信息
  • 保存整个订单聚合

这就带来了一个问题:无意义的写。我们会想:我只修改了订单详情中的某一个属性,但是却要保存整个聚合对象,须知:我的持久层为了保存这一个对象可能要保存很多张表(我们这里仅讨论关系型数据库,对于nosql我们不予探讨),这样性能又怎么保证呢?
看一下数据流更加清晰:
DDD 战术设计落地重难点——分包,分层,依赖关系,持久化设计_第5张图片
从dao查询到PO,接着转化成DO后在domain层进行更新修改成DO’,接着再转换成PO’,使用dao进行持久化操作。
看了这个图就很清晰了,我能不能这么玩:
DDD 战术设计落地重难点——分包,分层,依赖关系,持久化设计_第6张图片
直接通过判断PO和PO‘的差别,然后比对修改的属性,并将其持久化到数据库,这将大大减小数据库保存带来的性能消耗!

  • 实现
    这种方案仅限于关系型数据库,因此我打算将方案的实现放在仓储的实现中(虽然这个在一定程度上使得仓储实现变成有状态的了,即缓存了PO对象),我们知道,Spring Bean是单例的,而对于每个请求的处理则是多个线程,因此我们可以将PO原始对象缓存在线程的ThreadLocal对象中。
  • 优点
    • 大大减少了数据库存储的性能消耗;
  • 缺点
    • 在某种意义上来说,它为持久层引入了状态,使得仓储是"有状态"的;
    • 实现略微复杂。

结语

最后再引流一波,关于DDD的具体落地实现参照开源项目 Mall-Vue branch:Mall-CI 。目前后端尚处于开发状态,欢迎star&fork。

你可能感兴趣的:(Java#架构,java#微服务,java,微服务架构)