DDD该怎么去落地实现(1)关键是“关系”

DDD落地的关键是“关系”

这些年,我认为DDD走到了一个死胡同里了,因为落地实现过于困难。很多团队在经过一段时间的学习,清楚理解了DDD那些晦涩的概念,根据业务绘制出领域模型,这都不困难。但绘制领域模型不是我们最终的目的,最终的目的是基于领域模型,设计开发出业务系统。这时,很多团队就犯难了。这里面的关键问题就在于,采用DDD传统的做法,软件开发过于复杂,使得开发工作量不仅没有降低,甚至更高了。编写代码的增大,不仅使第一次的开发工作量增加,更要命的是日后变更维护的工作量增大,使得日后的维护变得困难。

采用了DDD,应当使得我们的开发变得简单,代码变得清爽,而不是代码变得臃肿。因此,我认为DDD也需要适当地变革,通过将一些通用的代码下沉底层平台,简化软件的开发。只有开发变得简单了,开发工作量减少了,DDD才能真正推行下去。我将通过一系列的文章,探讨一下DDD如何简化,我的设计思路,希望给大家带来帮助。

今天,首先谈谈模型对象间的关系,以及如何落地设计编码。在DDD的软件开发模式中,其实划分为两个阶段:基于业务的领域建模、基于领域模型的编码实现。在第一个阶段中,DDD要求我们首先深入地理解需求背后的业务,深入地掌握业务领域知识,然后将我们对业务的理解形成领域模型。在这个阶段中,我们会先划分限界上下文,将纷繁复杂的业务划分成一个一个较小的区域。然后,基于每个限界上下文的业务进行领域建模,形成如下的领域模型:

注意,在这个领域模型中,包含许多领域对象,如订单、用户、地址。每个领域对象包含各种的属性和方法,以及相互的关系。譬如,一个订单对应一个用户,但一个用户可以有多个订单,因此从订单到用户是一个“多对一”关系,有一个从订单多用户的箭头。这个箭头什么意思呢?它代表在订单对象中有一个属性是“用户”,并且是一个指针指向用户对象,落地到程序就是这样写的:

@Data
@EqualsAndHashCode(callSuper = true)
public class Order extends Entity<Long> {private Long id;private Long customerId;private Long addressId;private Double amount;private Date orderTime;private Date modifyTime;private String status;private Customer customer;private Address address;private Payment payment;private List<OrderItem> orderItems;...
}

在这个模型中,订单对象有4个箭头,指向“用户”、“用户地址”、“支付”和“订单明细”。这个箭头称为“导航”,它代表对象和对象之间的关系。然而,这些关系的类型是不一样的,从订单到用户和用户地址是“多对一”关系;从订单到支付是“一对一”关系;从订单到订单明细是“一对多”关系。

但现在的问题就在于,“一对一”和“多对一”关系,在这段代码中都是一个指针变量,我们无法从代码上区分它们到底是什么类型。同样,“一对多”关系在代码上是一个集合指针,如这里的订单,我们同样无法区分它到底是“一对多”还是“多对多”关系。也就是说,DDD要求我们将领域模型的原貌直接映射成代码实现。但真正代码实现时,仅仅写一个领域对象不能把领域对象间的关系描述清楚。所以,我们必须要在领域对象的基础上进行补充说明,来说明对象间的关系。我们通过一个DSL来描述。

DSL(Domain Specific Language,领域特定语言)是针对某个特定领域的计算机程序设计语言。在DDD中的DSL就是领域驱动设计这个特定领域的计算机设计语言,我们可以用xml、yaml、json等任何格式的文档来表达,但它表达的是领域驱动中的“对象”、“对象的属性”、“对象间的关系”,以及如何与数据库映射等相关的内容。

<do class="com.edev.trade.order.entity.Order" tableName="t_order"><property name="id" column="id" isPrimaryKey="true"/><property name="customerId" column="customer_id"/><property name="addressId" column="address_id"/><property name="amount" column="amount"/><property name="orderTime" column="order_time"/><property name="modifyTime" column="modify_time"/><property name="status" column="status"/><join name="customer" joinKey="customerId" joinType="manyToOne"class="com.edev.trade.order.entity.Customer"/><join name="address" joinKey="addressId" joinType="manyToOne"class="com.edev.trade.order.entity.Address"/><join name="payment" joinType="oneToOne" isAggregation="true"class="com.edev.trade.order.entity.Payment"/><join name="orderItems" joinKey="orderId" joinType="oneToMany"isAggregation="true" class="com.edev.trade.order.entity.OrderItem"/>
</do>

这里可以看到,通过一个xml文件,描述了订单对象的所有补充信息。在这里有订单对象的所有属性,以及它的所有关系。在这些关系中,不仅描述了每个关系的类型,还描述了它的关联属性,以及是否是聚合关系。与此同时,在这个DSL中还描述了订单对象对应的数据库表,以及每个字段对应的订单对象中的属性。有了这个DSL,才真正将领域模型的原貌映射到设计编码中。

除了设计编码,领域模型还要落地到数据库设计中。因此,DDD落地实现的关键是正确地识别领域对象间的“关系”。只有在领域建模的过程中正确地识别这些关系,才能在后面的程序编码与数据库设计中,做出正确的设计。这里的关系有“一对一”、“多对一”、“一对多”、“多对多”以及继承关系,我们一个一个来讨论吧。

首先是一对一关系。订单与它的支付就是一对“一对一”关系,一个订单只能有一个支付,而每个支付都要对应一个订单。这里其实有2个约束:每个支付必须对应一个订单,在数据库中通过一个外键来表示;一个订单只能有一个支付,这是一个唯一性约束,因此将刚才的那个外键变为了支付表的主键。最后的效果就是,订单表的主键变成了支付表的主键。通过这个主键,每条支付记录都要对应一个订单,但有些订单可能在支付表中没有记录。

值得注意的是,一对一关系的双方,到底谁指向谁,其实没有定理,必须基于相应的功能来决策。譬如,用户与会员也是一对一关系,然而应当谁指向谁呢?看后面的功能如何设计。如果将用户与会员当成彼此独立的模块,用户查询不显示会员,而会员查询却要显示对应用户,则会员指向用户;相反,如果将会员作为用户的子功能,都在用户档案中统一管理,则用户指向会员,查询用户时就能自动带出会员。当然,这样的设计会将用户指向会员的关系设计成聚合关系。

在一对一关系中,由于关系的双方都是通过主键来对应,因此在DSL中就不需要描述关联字段,只需要说明是一对一关系就行了。同时,如果是聚合关系,则在DSL中进行如下描述:

<join name="payment" joinType="oneToOne" isAggregation="true" class="com.edev.trade.order.entity.Payment"/>

接着,是多对一关系,它是领域建模中最常见的关系。如订单指向用户及用户地址、订单明细指向商品,都是多对一关系。在数据库设计时,通过一个外键就可以表示多对一关系。譬如,在以上案例中,在订单表中通过用户ID、地址ID,在订单明细表再通过商品ID,就能表示这种多对一关系。

在DSL中,多对一关系需要一个关联的字段。在订单对象中有一个用户ID,它就是用户对象的关联字段,在DSL中表示如下:

<join name="customer" joinKey="customerId" joinType="manyToOne" class="com.edev.trade.order.entity.Customer"/>

下一个关系是一对多关系,它实际上代表的是一种主子表的关系,如订单与订单明细、表单与表单明细、发票与发票明细。在这种关系中,订单只有一个,但它对应的订单明细却有多个。这样,在订单对象中的“订单明细”属性就不能是一个变量,而必须是一个集合变量,要么是Set,要么是List。这种关系如何映射成数据库设计呢?在数据库设计中没有一对多关系,然而将该关系倒过来,将订单明细指向订单,就变成了多对一关系。因此,在订单明细表中通过一个外键,就可以表示了。

这里就产生了一个问题了:在领域建模时,为什么不是订单明细指向订单呢?在这里实际上是一种聚合关系,订单是整体,订单明细是部分。如果设计成订单明细指向订单,那么这种聚合关系就消失了,我们必须得分别去管理订单和订单明细的增删改。在添加订单明细时,必须要检查它对应的订单是否存在;在删除订单时,也要检查它对应的订单明细是否已经被删除掉了,这样就会增大软件设计的复杂度。而将订单指向订单明细,则可以通过聚合关系将对订单明细的管理,封装在对订单的管理中。在增删改订单的同时,就在管理对订单明细的增删改,那么设计编码就得到了简化。代码简化了,日后的变更维护也就变得简单了。有了这样的设计,在编码的时候,首先将订单对象中增加一个订单明细的集合变量,然后在DSL中进行如下配置:

<join name="orderItems" joinKey="orderId" joinType="oneToMany" isAggregation="true" class="com.edev.emall.order.entity.OrderItem"/>

在这里,将该关系定义为了聚合关系。但毫无疑问,这种聚合关系的实现,需要底层“仓库”的支持。譬如,在更改订单时,我们如何知道客户是否对订单明细有增删改操作呢?这些问题我们在后面的文章中再与大家探讨。除此之外,以上案例的代码详见我的仓库:

Order.jave

order.xml

除了以上三个关系以外,还有“多对多”与继承关系。这两种关系在设计上都比较复杂,我将在下一期跟大家探讨。

相关的文章:

DDD你真的理解清楚了吗?怎么准确理解“值对象”

DDD你真的理解清楚了吗?充血模型 or 贫血模型

DDD你真的理解清楚了吗?非常抽象的聚合

(待续)

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

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

RabbitMQ---面试题

常见面试题 1.MQ的作用及应用场景 类似问题&#xff1a;项目什么情况下用到了MQ&#xff0c;为什么要用MQ MQ的主要应用场景&#xff0c;消息队列的应用场景&#xff0c;为什么说消息队列可以削峰 首先MQ是一种用来接收和转发消息的队列&#xff0c;常见的应用常见如下&…

PaddleSeg 从配置文件和模型 URL 自动化运行预测任务

git clone https://github.com/PaddlePaddle/PaddleSeg.git# 在ipynb里面运行 cd PaddleSegimport sys sys.path.append(/home/aistudio/work/PaddleSeg)import os# 配置文件夹路径 folder_path "/home/aistudio/work/PaddleSeg/configs"# 遍历文件夹&#xff0c;寻…

2_高并发内存池_各层级的框架设计及ThreadCache(线程缓存)申请内存设计

一、高并发内存池框架设计 高并发池框架设计&#xff0c;特别是针对内存池的设计&#xff0c;需要充分考虑多线程环境下&#xff1a; 性能问题锁竞争问题内存碎片问题 高并发内存池的整体框架设计旨在提高内存的申请和释放效率&#xff0c;减少锁竞争和内存碎片。 高并发内存…

WebODM之python实现

1、安装webodm_slam 主要是了解API文档,查看之前的文章 安装WebODM_slate 2、安装webodm 查看之前的文章 Win10安装WebODM和操作全流程 3、python脚本 项目案例 This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. If a copy of…

2025年美赛C题:奥运奖牌榜模型 解析及Python代码实现

2025年美赛C题&#xff1a;奥运奖牌榜模型 解析及Python代码实现 1 题目 2025 MCM问题C&#xff1a;奥运奖 牌榜模型在最近的2024年巴黎夏季奥运会期间&#xff0c; 粉丝们除了观看个人项目外&#xff0c; 还关注每个 国家的整体 “ 奖牌榜 ”。 最终的结果&#xff08;表1&am…

Qt 5.14.2 学习记录 —— 십칠 窗口和菜单

文章目录 1、Qt窗口2、菜单栏设置快捷键添加子菜单添加分割线和菜单图标 3、工具栏 QToolBar4、状态栏 QStatusBar5、浮动窗口 QDockWidget 1、Qt窗口 QWidget&#xff0c;即控件&#xff0c;是窗口的一部分。在界面中创建控件组成界面时&#xff0c;Qt自动生成了窗口&#xf…

Spring Boot 邂逅Netty:构建高性能网络应用的奇妙之旅

一、引言 在当今数字化时代&#xff0c;构建高效、可靠的网络应用是开发者面临的重要挑战。Spring Boot 作为一款强大的 Java 开发框架&#xff0c;以其快速开发、简洁配置和丰富的生态支持&#xff0c;深受广大开发者喜爱。而 Netty 作为高性能、异步的网络通信框架&#xff…

Windows中本地组策略编辑器gpedit.msc打不开/微软远程桌面无法复制粘贴

目录 背景 解决gpedit.msc打不开 解决复制粘贴 剪贴板的问题 启用远程桌面剪贴板与驱动器 重启RDP剪贴板监视程序 以上都不行&#xff1f;可能是操作被Win11系统阻止 最后 背景 远程桌面无法复制粘贴&#xff0c;需要查看下主机策略组设置&#xff0c;结果按WinR输入…

深圳大学-智能网络与计算-实验一:RFID原理与读写操作

实验目的与要求 掌握超高频RFID标签的寻卡操作。掌握超高频RFID标签的读写操作。掌握超高频RFID标签多张卡读取时的防冲突机制。 方法&#xff0c;步骤 软硬件的连接与设置超高频RFID寻卡操作超高频RFID防冲突机制超高频RFID读写卡操作 实验过程及内容 一&#xff0e;软硬…

快门:凝固瞬间与塑造动感的魔法开关

目录 一、快门的基本概念 二、快门速度的分类及效果 &#xff08;一&#xff09;高速快门 &#xff08;二&#xff09;低速快门 &#xff08;三&#xff09;安全快门 三、快门优先模式&#xff1a;掌控拍摄节奏的利器 四、快门与其他摄影要素的关系 &#xff08;一&…

2025发文新方向:AI+量化 人工智能与金融完美融合!

2025深度学习发论文&模型涨点之——AI量化 人工智能的融入&#xff0c;使量化交易实现了质的突破。借助机器学习、深度学习等先进技术&#xff0c;人工智能可高效处理并剖析海量市场数据&#xff0c;挖掘出数据背后错综复杂的模式与趋势&#xff0c;从而不仅提升了数据分析…

单链表算法实战:解锁数据结构核心谜题——链表的回文结构

题目如下&#xff1a; 解题过程如下&#xff1a; 回文结构举例&#xff1a; 回文数字&#xff1a;12521、12321、1221…… 回文字符串&#xff1a;“abcba”、“abba”…… 并不是所有的循环嵌套的时间复杂度都是O(n^2) 可以用C写C程序&#xff1a; C里可以直接使用ListNode…

golang网络编程

socket编程 socket图解 Socket是BSD UNIX的进程通信机制&#xff0c;通常也称作”套接字”&#xff0c;用于描述IP地址和端口&#xff0c;是一个通信链的句柄。Socket可以理解为TCP/IP网络的API&#xff0c;它定义了许多函数或例程&#xff0c;程序员可以用它们来开发TCP/IP网…

「 机器人 」仿生扑翼飞行器中的“被动旋转机制”概述

前言 在仿生扑翼飞行器的机翼设计中,模仿昆虫翼的被动旋转机制是一项关键技术。其核心思想在于:机翼旋转角度(攻角)并非完全通过主动伺服来控制,而是利用空气动力和惯性力的作用,自然地实现被动调节。以下对这种设计的背景、原理与优势进行详细说明。 1. 背景:昆虫的被动…

git远程仓库如何修改

1.需要做的事情&#xff1a;把git的远程仓库修改掉&#xff0c;在git创建一个自己的仓库 如果你是私有化的话&#xff0c;可以生成一个自己token令牌也可以。到时候push的时候会让你登录你就可以输入你的token令牌和用户名。 2.查看当前仓库的远程地址是不是自己的 &#xff…

罗氏线圈的学习【一】

TI的罗氏线圈介绍&#xff0c;讲解的非常好&#xff1a; 具有低功耗低成本性能的PCB罗氏线圈与积分电路设计 罗氏线圈&#xff08;Rogowski Coil&#xff09;是一种常见的电流测量装置&#xff0c;广泛用于高精度和非接触式的电流测量场景&#xff0c;尤其是在测量交流电流、…

计算机视觉-卷积

卷积-图像去噪 一、图像 二进制 灰度 彩色 1.1二进制图像 0 1 一个点可以用一个bit&#xff08;0/1&#xff09;来表示 1.2灰度图像 0-255 一个点可以用一个byte来表示 1.3彩色图像 RGB 表达一个彩色图像先说它的分辨率p/w&#xff08;宽&#xff09;和q/h&#xff08;高…

Ansys Thermal Desktop 概述

介绍 Thermal Desktop 是一种用于热分析和流体分析的通用工具。它可用于组件或系统级分析。 来源&#xff1a;CRTech 历史 Thermal Desktop 由 C&R Technologies (CR Tech) 开发。它采用了 SINDA/FLUINT 求解器。SINDA/FLUINT 最初由 CR Tech 的创始人为 NASA 的约翰逊航…

32、【OS】【Nuttx】OSTest分析(1):stdio测试(二)

背景 接上篇wiki 31、【OS】【Nuttx】OSTest分析&#xff08;1&#xff09;&#xff1a;stdio测试&#xff08;一&#xff09; 继续stdio测试的分析&#xff0c;上篇讲到标准IO端口初始化&#xff0c;单从测试内容来说其实很简单&#xff0c;没啥可分析的&#xff0c;但这几篇…

WPF基础 | 初探 WPF:理解其核心架构与开发环境搭建

WPF基础 | 初探 WPF&#xff1a;理解其核心架构与开发环境搭建 一、前言二、WPF 核心架构2.1 核心组件2.2 布局系统2.3 数据绑定机制2.4 事件处理机制 三、WPF 开发环境搭建3.1 安装 Visual Studio3.2 创建第一个 WPF 应用程序 结束语优质源码分享 WPF基础 | 初探 WPF&#xff…