当前位置: 首页 > news >正文

DDD该怎么去落地实现(3)通用的仓库和工厂

通用的仓库和工厂

我有一个梦,就是希望DDD能够成为今后软件研发的主流,越来越多研发团队都转型DDD,采用DDD的设计思想和方法,设计开发软件系统。这个梦想在不久的将来是有可能达成的,因为DDD是软件复杂性的解决之道,而今后的软件会越来越庞大,越来越复杂。然而,经过这几年的DDD热潮,阻碍各研发团队转型DDD的拦路虎是什么呢?我认为是DDD落地开发编码过于复杂,编码工作量大,阻碍了DDD的推广。试想,一个新的开发思想能降低研发的工作量,必然得到大家的普遍欢迎,推行就比较容易,而反之则非常困难,抵触情绪大。

那么,为什么DDD落地开发的成本很高,编码的工作量大呢?针对这个问题,我们要好好分析分析,进而找到解决的思路。按照DDD的开发规范,当团队经过一系列的业务梳理,形成领域模型以后,就开始以领域模型为核心,设计开发业务系统。如图所示是DDD的分层架构:

通过这张图可以看到,展现层就是前端界面(如WebUI、客户端程序、移动App等等),应用层就是Controller,领域层的服务就是Service,聚合、实体、值对象都是从领域模型中映射过来的领域对象,其它都是基础设施(其中也包括了上一期提到的仓库、工厂、缓存)。这样一个架构集中体现了“整洁架构”的设计思想,即将上层的业务代码与底层的技术框架通过分层进行分离,进而实现解耦。也就是说,只有领域层的服务Service、领域对象(聚合、实体、值对象)才能编写业务代码,实现业务规则、业务操作、业务流程,是领域模型的映射。其它层次的代码都是技术,包括前端UI、应用层的Controller、基础设施,以及这张图没有画出来的服务网关、数据库,等等。按照这样的设计思想,最后代码编写的效果就变成了这样:

在整个系统中,每个功能都必须要有各自的Controller、Service和该功能所需的领域对象(聚合、实体、值对象),然后在持久化到数据库的时候,还得有对应的仓库、工厂、缓存。并且,每个功能的仓库、工厂、缓存都不一样。譬如,订单仓库中存在聚合,在增删改订单表的同时,还有增删改订单明细表,而库存仓库只需要管库存表的增删改;订单工厂在查询的时候需要装配与订单相关的客户、地址、订单明细,而库存工厂只需要装配与库存相关的商品对象。正因为如此,每个功能在开发的时候,都需要编写大量代码。

不仅如此,在不同层次中,数据是存储在不同格式的数据对象中,因此数据在各个层次中流转时,还要编写各种格式的数据对象及转换程序,在Json, DTO, DO与PO中转换数据。这样一套下来,DDD的软件开发就麻烦死了,如果我是程序员,我也会不胜其烦。这就是DDD目前的问题所在。

很多时候就是这样的,当分析和查找到问题以后,就离解决问题不远了。我的思路就是,通过一个底层平台(如低代码平台)将DDD中那些繁杂的操作统一起来,实现集约化,那么开发人员就只需要去编写那些各自的业务代码,那么工作量不就变小了吗?也就是说,如果数据接入层、应用层、基础设施层都通过平台实现了,开发人员就只需要编写领域层的领域服务Service和领域对象(聚合、实体、值对象),以及前端的UI界面,开发人员的工作量就减少了,就可以更加专注地按照领域模型去设计编码,DDD不就更容易落地了。

这的确是一个非常完美的思路,然而要实现这个思路,很显然需要一个支持DDD的强大底层平台。那么,这个强大的底层平台需要提供哪些功能呢?我认为有3个:通用的Controller、仓库及其工厂。按照CQRS(Command Query Responsibility Segregation)架构的设计思想,该平台可以划分成两部分设计:增删改(即命令)和查询,我们先看看增删改的设计思路。

在业务系统实现增删改的操作(即命令操作)时,和过去一样,每个功能在前端都有各自的UI。但和过去不一样的是,所有的UI在请求后端时,都是请求的那一个Controller(即OrmController)。前端请求的Url中包含了要请求的功能,因此这个Controller通过反射去请求后端的Service,并自动将Json转换为领域对象。这里有一个很神奇的事情就是,这个Controller怎么能自动将Json转换为领域对象呢?我们可以在开发规范上规定,前端请求后端时,Json对象必须与后台的领域对象保持一致。如前端提交订单,其Json对象长这样:

{
  "id": 1,
  "customerId": 10001,
  "addressId": 1000100,
  "orderItems": [
    {
      "id": 10,
      "productId": 30001,
      "quantity": 2
    }
  ]
}

可以看到,Json对象不必包含领域对象的所有属性,而是必要属性。后端的OrmController收到这个Json以后,就可以通过DDD工厂去读取DSL,将Json转换为订单对象,并请求OrderService中的create()方法,就可以完成创建订单的操作。当然,如果用户请求的是“下单”,要做的就不仅仅是创建订单,还有支付、库存扣减等操作,需要分布式事务,因此请求的是OrderAggService的placeOrder()方法,详细的设计详见测试用例:

OrderService的测试用例

OrderAggService的测试用例

接着,整个业务操作都在领域层的Service和领域对象中进行(详见《充血模型 or 贫血模型》)。当所有的业务操作的执行完以后,通过仓库进行数据持久化。这时,所有的Service都注入了通用仓库,由它去完成相关的增删改操作(详见上一期的通用仓库设计思路)。

有了DDD的底层平台的支持,所有的领域对象和Service完成的都是对业务的操作,它们只知道领域对象长什么样,只对领域对象进行操作,并不知道后面有数据库,从而实现整洁架构中业务与技术的解耦。接着,将增删改等数据库操作交给底层的通用仓库,包括聚合关系的增删改,实现了CQRS中的“C”。

那么,领域对象的查询(即CQRS中的“Q”)又该如何实现呢?按照DDD的设计思想,当我们将领域模型中对象的关系映射到程序中领域对象的关系以后,在查询领域对象时,底层也要保持这种关系。也就是说,当查询订单时,底层不仅仅是查询订单表,还要查询与订单相关的用户表、地址表与订单明细表,最后将它们装配成一个完整的订单对象。这个查询与装配的工作就交给了DDD的“工厂”来完成。

同样,过去的DDD编码实现,需要为每个领域对象编写工厂,来完成这个查询与装配的工作,这无疑会增加DDD的开发工作量。为了简化DDD的编码,降低落地难度,我们的思路同样是由底层提供一个通用的工厂,所有对领域对象的查询统统都交给这个通用工厂。当通用工厂要查询数据时,先查找DSL获取该对象对应的数据库表与所有的关系。然后,通用工厂根据这些信息,依次到数据库各对应表中去进行查询。最后,再依据DSL进行装配,返回一个完整的领域对象。这个领域对象在返回给Service前,还会在仓库中进行缓存,以提高下次查询的效率。所以,通用工厂不直接面向Service,而是被通用仓库封装。通用仓库封装了通用仓库与缓存,就可以完成上层Service的所有增删改与查询的操作。

譬如,现在要实现对订单的查询,首先通过MyBatis去编写一个mapper:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"   
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.edev.emall.query.dao.CustomerMapper">
    <sql id="select">
       SELECT * FROM t_customer WHERE 1 = 1
    </sql>
    <sql id="conditions">
       <if test="id != '' and id != null">
          and id in (${id})
       </if>
       <if test="value != '' and value != null">
          and (id like '%${value}' or name like '%${value}')
       </if>
       <if test="gender != '' and gender != null">
          and gender = #{gender}
       </if>
    </sql>
    <sql id="isPage">
       <if test="size != null  and size !=''">
          limit #{size} offset #{firstRow} 
       </if>
    </sql>
    <select id="query" parameterType="java.util.HashMap" resultType="java.util.HashMap">
       <include refid="select"/>
       <include refid="conditions"/>
       <include refid="isPage"/>
    </select>
    <select id="count" parameterType="java.util.HashMap" resultType="java.lang.Long">
       select count(*) from (
          <include refid="select"/>
          <include refid="conditions"/>
       ) count
    </select>
    <select id="aggregate" parameterType="java.util.HashMap" resultType="java.util.HashMap">
       select ${aggregation} from (
          <include refid="select"/>
          <include refid="conditions"/>
       ) aggregation
    </select>
</mapper>

在这个mapper中可以看到,对订单的查询就只查询订单表,而不进行其它任何的关联。接着,在spring中进行如下的装配:

@Configuration
public class QryConfig {
    @Autowired @Qualifier("basicDaoWithCache")
    private BasicDao basicDaoWithCache;
    @Autowired @Qualifier("repositoryWithCache")
    private BasicDao repositoryWithCache;
    @Bean
    public QueryDao customerQryDao() {
        return new QueryDaoMybastisImplForDdd(
                "com.edev.emall.customer.entity.Customer",
                "com.edev.emall.query.dao.CustomerMapper");
    }
    @Bean
    public QueryService customerQry() {
        return new AutofillQueryServiceImpl(
                customerQryDao(), repositoryWithCache);
    }
    @Bean
    public QueryDao accountQryDao() {
        return new QueryDaoMybastisImplForDdd(
                "com.edev.emall.customer.entity.Account",
                "com.edev.emall.query.dao.AccountMapper");
    }
    @Bean
    public QueryService accountQry() {
        return new AutofillQueryServiceImpl(
                accountQryDao(), basicDaoWithCache);
    }
}

先装配一个QueryDao,它有一个QueryDaoMybastisImplForDdd的实现类,通过MyBatis对订单进行查询,然后按照DDD返回订单对象的列表。接着,再注入到QueryService中,它有一个AutofillQueryServiceImpl的实现类。这样,当QueryDao通过分页查询出这一页的20条记录以后,AutofillQueryServiceImpl就会根据DSL进行数据补填,将这每一个订单对象的用户、地址、明细,都到数据库中进行查询,然后完成补填与装配,最后获得一个完整的领域对象列表。有了这样的设计,每一个模块的查询都变得简单了。你只需要按照领域模型先形成领域对象和DSL,然后编写一个MyBatis的mapper,进行spring的装配,查询的开发工作就完成了。注意,AutofillQueryServiceImpl的第二个参数是在进行补填时,用谁来查询并补填。如果要补填的对象里还有关系(如地址里有省、市、县的关联),则选择repositoryWithCache,否则就用basicDaoWithCache。

最后,每个查询功能都有各自的UI界面,但它们在查询时,请求的都是这一个Controller(即QueryController)。这样,通过领域建模,通过一些简单的配置就可以快速完成各个模块的查询功能。

有了这个平台,按照DDD的设计思想,我们只需要进行领域建模,将我们对业务的理解形成领域模型,就可以快速完成软件的开发。如今有了AI编程,甚至可以训练一个Agent,我们只要深入地理解业务,形成领域模型,就可以让AI按照这样的思路快速开发系统。有了这样的思路,不仅可以让我们将更多的精力放到业务理解而不是软件开发,给我们减负,又可以给AI编程制定规范,有利于日后长期的维护与变更。相信在这样的背景下,DDD又可以焕发生机,成为日后软件开发的主流。

(待续)

相关文章:

  • 用大模型学大模型04-模型可视化与数据可视化
  • [数据结构]二叉搜索树详解
  • Spring——Spring开发实战经验(4)
  • SpringBoot 的核心只有几张图
  • Ubuntu 24.04.1 LTS 本地部署 DeepSeek 私有化知识库
  • C语言中的强制类型转换:原理、用法及注意事项
  • 1.buuctf [BJDCTF2020]EasySearch
  • Hadoop之HDFS的使用
  • 从零开始:Gitee 仓库创建与 Git 配置指南
  • 服务器硬件知识--------linux系统初识and安装
  • Linux csplit 命令实现日志文件的拆分
  • 软考高级《系统架构设计师》知识点(五)
  • Spring事务原理的具体实现,以及包括源码以及具体在实际项目中的使用。
  • 【etcd】etcd_APIs 简单KV、watch、lease、txn命令
  • 数据结构-顺序表
  • 东方财富股吧发帖与评论爬虫
  • 基于腾讯云TI-ONE 训练平台快速部署和体验 DeepSeek 系列模型
  • 「AI学习笔记」机器学习与深度学习的区别:从技术到产品的深度解析(四)...
  • 如何高效利用 AI 工具提升开发效率?
  • 机器学习PCA和LDA
  • 中国人民解放军南部战区位南海海域进行例行巡航
  • 新开发银行如何开启第二个“金色十年”?
  • 中信银行一季度净利195.09亿增1.66%,不良率持平
  • 深入贯彻中央八项规定精神学习教育中央指导组培训会议召开
  • 解读|特朗普“助攻”下加拿大自由党“惨胜”,卡尼仍需克服“特鲁多阴影”
  • 费高云调研党的建设工作:营造风清气正劲足的政治生态