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

news2025/3/23 8:46:58

通用的仓库和工厂

我有一个梦,就是希望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又可以焕发生机,成为日后软件开发的主流。

(待续)

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2300226.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【SpringBoot苍穹外卖】debugDay0 打开前端页面

在某一天学完后&#xff0c;电脑关机&#xff0c;再打开啥都忘了&#xff0c;记起来一点点&#xff0c;前端页面打不开&#xff0c;后端控制台一直循环出错。原来是下面这样哈哈。 查看端口是否被别的程序占用的操作步骤 winR输入cmd打开命令行 netstat -ano | findstr "8…

做谷歌SEO需要了解哪些基本概念?

做谷歌SEO时&#xff0c;必须掌握一些基本的概念。首先是关键词。关键词是用户在搜索框里输入的词汇&#xff0c;它们直接影响到你网站的排名。所以&#xff0c;了解用户的搜索习惯、挑选合适的关键词&#xff0c;是每一个SEO优化者必须做的工作。 内容是关键。谷歌非常看重网…

通过BingAPI爬取Bing半个月内壁纸

通过BingAPI爬取Bing半个月内壁纸 一、前言二、爬虫代码三、代码说明 一、前言 爬取Bing搜索网站首页壁纸的方式主要有两种&#xff0c;第一种为间接爬取&#xff0c;即并不直接对Bing网站发起请求&#xff0c;而是对那些收集汇总了Bing壁纸的网站发起请求&#xff0c;爬取图片…

springboot021-基于协同过滤算法的个性化音乐推荐系统

&#x1f495;&#x1f495;作者&#xff1a; 小九学姐 &#x1f495;&#x1f495;个人简介&#xff1a;十年Java&#xff0c;Python美女程序员一枚&#xff0c;精通计算机专业前后端各类框架。 &#x1f495;&#x1f495;各类成品Java毕设 。javaweb&#xff0c;ssm&#xf…

开关电源实战(一)宽范围DC降压模块MP4560

系列文章目录 文章目录 系列文章目录MP4560MP4560 3.8V 至 55V 的宽输入范围可满足各种降压应用 MOSFET只有250mΩ 输出可调0.8V-52V SW:需要低VF肖特基二极管接地,而且要靠近引脚,高压侧开关的输出。 EN:输入使能,拉低到阈值以下关闭芯片,拉高或浮空启动 COMP:Compens…

【MySQL】我在广州学Mysql 系列——Mysql 日志管理详解

ℹ️大家好&#xff0c;我是练小杰&#xff0c;今天又是新的一周了&#xff0c;又该摆好心态迎接美好的明天了&#xff01;&#xff01;&#xff01;&#x1f606; 本文主要对Mysql数据库中的日志种类以及基本命令进行讨论&#xff01;&#xff01; 回顾&#xff1a;&#x1f4…

《Zookeeper 分布式过程协同技术详解》读书笔记-2

目录 zk的一些内部原理和应用请求&#xff0c;事务和标识读写操作事务标识&#xff08;zxid&#xff09; 群首选举Zab协议&#xff08;ZooKeeper Atomic Broadcast protocol&#xff09;文件系统和监听通知机制分布式配置中心, 简单Demojava code 集群管理code 分布式锁 zk的一…

HTML5+CSS多层级ol标签序号样式问题

在CSS中&#xff0c;ol标签用于创建有序列表&#xff0c;而多层级的ol标签可以通过CSS实现不同的序号样式。以下是一些常见的问题和解决方案&#xff1a; 1. 多层级ol的序号格式问题 默认情况下&#xff0c;多层级的ol标签会自动继承父级的序号格式&#xff0c;但有时我们可能…

网络初始2:网络编程--基于UDP和TCP实现回显器

基础概念 1.发送端与接受端 在通过网络传输信息时&#xff0c;会有两个进程&#xff0c;接收端和发送端。 发送端&#xff1a;数据的发送方进程&#xff0c;即网络通信中的源主机。 接收端&#xff1a;数据的接收方进程&#xff0c;即网路通信中的目的主机。 2.Socet套接字…

vtkCamera类的Dolly函数作用及相机拉近拉远

录 1. 预备知识 1.1.相机焦点 2. vtkCamera类的Dolly函数作用 3. 附加说明 1. 预备知识 要理解vtkCamera类的Dolly函数作用,就必须先了解vtkCamera类表示的相机的各种属性。  VTK是用vtkCamera类来表示三维渲染场景中的相机。vtkCamera负责把三维场景投影到二维平面,如…

车载音频架构图详解(精简)

目录 上图是车载音频架构图,对这个图我们进行详细的分析 左边第一层 是 app 常用的类有MediaPlayer和MediaRecorder, AudioTrack和AudioRecorder 第二层 是framework提供给应用的多媒体功能的AP

使用神经网络对驾驶数据进行道路类型分类

摘要 道路分类&#xff0c;了解我们是在城市、农村地区还是在高速公路上驾驶&#xff0c;可以提高现代驾驶员辅助系统的性能&#xff0c;并有助于了解驾驶习惯。本研究的重点是仅使用车速数据来普遍解决这个问题。已经开发了一种数据记录方法&#xff0c;用于为 On-board Diagn…

S4D480 S4HANA 基于PDF的表单打印

2022年元旦的笔记草稿 SAP的表单打印从最早的SAPScripts 到后来的SMARTFORM&#xff0c;步入S4时代后由于Fiori的逐渐普及&#xff0c;更适应Web的Adobe Form成了SAP主流output文件格式。 目录 一、 基于PDF表单打印系统架构Interface 接口Form 表单ContextLayout 二、表单接…

qt QOpenGLTexture详解

1. 概述 QOpenGLTexture 是 Qt5 提供的一个类&#xff0c;用于表示和管理 OpenGL 纹理。它封装了 OpenGL 纹理的创建、分配存储、绑定和设置像素数据等操作&#xff0c;简化了 OpenGL 纹理的使用。 2. 重要函数 构造函数&#xff1a; QOpenGLTexture(const QImage &image,…

Deepseek-R1推理模型API接入调用指南 ChatGPT Web Midjourney Proxy 开源项目接入Deepseek教程

DeepSeek-R1和OpenAI o1模型都属于推理任务模型&#xff0c;两个模型各有优点&#xff1a;DeepSeek-R1 在后训练阶段大规模使用了强化学习技术&#xff0c;在仅有极少标注数据的情况下&#xff0c;极大提升了模型推理能力。在数学、代码、自然语言推理等任务上&#xff0c;性能…

蓝耘智算携手DeepSeek,共创AI未来

&#x1f31f; 各位看官号&#xff0c;我是egoist2023&#xff01; &#x1f30d; 种一棵树最好是十年前&#xff0c;其次是现在&#xff01; &#x1f680; 今天来学习如何通过蓝耘智算使用DeepSeek R1模型 &#x1f44d; 如果觉得这篇文章有帮助&#xff0c;欢迎您一键三连&a…

【网络编程】之数据链路层

【网络编程】之数据链路层 数据链路层基本介绍基本功能常见协议 以太网什么是以太网以太网协议帧格式数据链路层的以太网帧报文如何封装/解封装以及分用以太网通信原理传统的以太网与集线器现代以太网与交换机碰撞域的概念 Mac地址基本概念为什么要使用Mac地址而不是使用IP地址…

EasyExcel 复杂填充

EasyExcel ​Excel表格中用{}或者{.} 来表示包裹要填充的变量&#xff0c;如果单元格文本中本来就有{、}左右大括号&#xff0c;需要在括号前面使用斜杠转义\{ 、\}。 ​代码中被填充数据的实体对象的成员变量名或被填充map集合的key需要和Excel中被{}包裹的变量名称一致。 …

通过VSCode直接连接使用 GPT的编程助手

GPT的编程助手在VSC上可以直接使用 选择相应的版本都可以正常使用。每个月可以使用40条&#xff0c;超过限制要付费。 如下图对应的4o和claude3.5等模型都可以使用。VSC直接连接即可。 配置步骤如下&#xff1a; 安装VSCODE 直接&#xff0c;官网下载就行 https://code.vis…

【算法与数据结构】并查集详解+题目

目录 一&#xff0c;什么是并查集 二&#xff0c;并查集的结构 三&#xff0c;并查集的代码实现 1&#xff0c;并查集的大致结构和初始化 2&#xff0c;find操作 3&#xff0c;Union操作 4&#xff0c;优化 小结&#xff1a; 四&#xff0c;并查集的应用场景 省份…