​2:DDD概念大白话

产品代码都给你看了,可别再说不会DDD(二):DDD概念大白话 #

这是一个讲解DDD落地的文章系列,作者是《实现领域驱动设计》的译者滕云。本文章系列以一个真实的并已成功上线的软件项目——码如云(https://www.mryqr.com)为例,系统性地讲解DDD在落地实施过程中的各种典型实践,以及在面临实际业务场景时的诸多取舍。

本系列包含以下文章:

  1. DDD入门
  2. DDD概念大白话(本文)
  3. 战略设计
  4. 代码工程结构
  5. 请求处理流程
  6. 聚合根与资源库
  7. 实体与值对象
  8. 应用服务与领域服务
  9. 领域事件
  10. CQRS

案例项目介绍 #

既然DDD是“领域”驱动,那么我们便不能抛开业务而只讲技术,为此让我们先从业务上了解一下贯穿本文章系列的案例项目 —— 码如云(不是马云,也不是码云)。如你已经在本系列的其他文章中了解过该案例,可跳过。

码如云是一个基于二维码的一物一码管理平台,可以为每一件“物品”生成一个二维码,并以该二维码为入口展开对“物品”的相关操作,典型的应用场景包括固定资产管理、设备巡检以及物品标签等。

在使用码如云时,首先需要创建一个应用(App),一个应用包含了多个页面(Page),也可称为表单,一个页面又可以包含多个控件(Control),比如单选框控件。应用创建好后,可在应用下创建多个实例(QR)用于表示被管理的对象(比如机器设备)。每个实例均对应一个二维码,手机扫码便可对实例进行相应操作,比如查看实例相关信息或者填写页面表单等,对表单的一次填写称为提交(Submission);更多概念请参考码如云术语。

在技术上,码如云是一个无代码平台,包含了表单引擎、审批流程和数据报表等多个功能模块。码如云全程采用DDD完成开发,其后端技术栈主要有Java、Spring Boot和MongoDB等。

码如云的源代码是开源的,可以通过以下方式访问:

码如云源代码:GitHub - mryqr-com/mry-backend: 本代码库为码如云后端代码。码如云是一个基于二维码的一物一码管理平台,可以为每一件“物品”生成一个二维码,手机扫码即可查看物品信息并发起相关业务操作,操作内容可由你自己定义,典型的应用场景包括固定资产管理、设备巡检以及物品标签等。在技术上,码如云是一个无代码平台,全程采用DDD、整洁架构和事件驱动架构思想完成开发。

DDD概念大白话 #

本文是本系列的第二篇文章,主要解释DDD中的各种概念,一方面让读者对DDD有个全景式的认识,另一方面也方便读者更好地理解本系列的后续文章。

DDD中的概念,说多不多,说少不少,一个新手在面对各种DDD名词的轮番轰炸时可能会被搞得晕头转向不知所措,而对于深谙其道的人来说,DDD也就那么点儿东西。除此之外,不同人对于DDD概念的理解也存在千差万别。本文尝试通过朴素的大白话解释DDD中的各种概念,不装,也不作。

DDD分为战略设计战术设计,战略设计是一种宏观的顶层设计,而战术设计则更偏向于代码落地实践。

战略设计中有通用语言、领域、子域和限界上下文等概念,这些概念不好理解得清楚,也不好讲得清楚。但是,从本质上讲,DDD的战略设计只在解决一个问题,即软件的模块化划分的问题。为此,我们将在下一篇战略设计中进行详细阐述。

当我们把软件的模块划分好(也即完成了战略设计)之后,下一步自然是编码实现了,于是乎我们也就顺理成章地进入了DDD的战术设计范畴。DDD的战术设计包括聚合根、实体和资源库等众多概念,其中最重要的当属聚合根了,那接下来我们就从聚合根讲起。

在上一篇DDD入门中我们提到,DDD的一个重要使命是实现软件中“业务复杂度”和“技术复杂度”的分离。为此,我们使用领域模型来描述业务,使之与数据库、消息队列等技术实现解耦开来。在DDD的领域模型中,最核心的则是聚合根(Aggregate Root),我们甚至可以认为整个DDD都是围绕聚合根的设计与实现展开的。

聚合根中的“聚合”即“高内聚,低耦合”中的“内聚”之意,而“根”则是“根部”的意思。事实上,并不存在一个教科书式的对聚合根的理论定义,你可以将聚合根理解为一个系统中最重要的那些名词。为了给你一个直观的理解,我们先来看看以下聚合根的例子:

  • 在一个电商系统中,一个订单(Order)对象表示一个聚合根
  • 在一个CRM系统中,一个客户(Customer)对象表示一个聚合根
  • 在一个银行系统中,一次交易(Transaction)对象表示一个聚合根

怎么样,是不是至少对聚合根有了一个感性认识?这些名词是其所在的软件系统之所以存在的原因,设想淘宝中没有了订单的概念还能称之为电商系统吗?当然,一个概念是否能成为聚合根是根据其所处的业务场景而定的,我们将在后续文章聚合根和资源库中做详细解释。

聚合根是业务逻辑的主要载体,在理想情况下应该是业务的唯一载体。但是,万事皆有但是,有时将业务逻辑放到聚合根中是不合适的,甚至是不可行的。比如,在码如云中,在更新成员手机号时,需要先行检查该手机号是否已经被占用,此时成员聚合根自身并不具备检查其他成员手机号的功能,因此要将这部分逻辑放到成员本身上则显得不合适了,我们在上一篇DDD入门中也提到了这个例子。不过不要慌,针对这种情形,DDD给出了专门的概念 —— 领域服务(Domain Service)。领域服务是聚合根自身无法完成业务逻辑时的代替品,是不得已而为之的一个概念,通常用于处理一些跨聚合操作或者需要访问技术基础设施的场景。

在下例中,领域服务MemberDomainService中的changeMyMobile()方法实现了两处聚合根(Member)无法自身实现的功能:一是调用mryPasswordEncoder检查密码是否正确,二是调用memberRepository检查手机号是否重复。

    //领域服务:MemberDomainServicepublic void changeMyMobile(Member member, String newMobile, String password) {if (!mryPasswordEncoder.matches(password, member.getPassword())) {throw new MryException(PASSWORD_NOT_MATCH,"修改手机号失败,密码不正确。", "memberId", member.getId());}if (Objects.equals(member.getMobile(), newMobile)) {return;}if (memberRepository.existsByMobile(newMobile)) {throw new MryException(MEMBER_WITH_MOBILE_ALREADY_EXISTS,"修改手机号失败,手机号对应成员已存在。",mapOf("mobile", newMobile,"memberId", member.getId()));}member.changeMobile(newMobile, member.toUser());}

源码出处:com/mryqr/core/member/domain/MemberDomainService.java

需要提醒的是,请不要被领域服务名字中的“服务”迷惑了,领域服务依然是领域模型的一部分,因为它也实现了业务逻辑,更多关于领域服务的内容,请查看本系列的应用服务和领域服务一文。

从更广义上讲,聚合根属于实体的范畴。在DDD中,存在实体(Entity)和值对象(Value Object)是一对相互对立的概念,实体用于表示那些具有生命周期的“存在”,而值对象用于表示那些仅仅起描述性作用的东西。实体通过唯一标识进行标定,而值对象则通过其包含的所有属性进行标定。在编码实现时,最直观的区别则是实体对象有ID,而值对象没有ID;此外,实体对象一般包含比较复杂的业务逻辑,而值对象通常则是一些简单的小对象,业务逻辑相对简单。举个常见的例子,无论一对双胞胎长得多么的相像,但由于两个人的身份证号不同(即ID不同),那么两人便属于不同的实体;而对于货币来说,一张崭新的百元大钞和一张破旧的占满了细菌的百元大钞是可以等价交换的,因为他们所包含的属性值(均是100元)是一样,因此他们均属于值对象。

在码如云中,管理员可以对表单提交(Submission)进行审批(Approval),审批结果存放在SubmissionApproval对象中,该对象则是一个值对象:

@Value
@Builder
@AllArgsConstructor(access = PRIVATE)
public class SubmissionApproval {private final boolean passed;//审批是否通过private final String note;//审批意见private final Instant approvedAt;//审批时间private final String approvedBy;//审批人ID
}

源码出处:com/mryqr/core/submission/domain/SubmissionApproval.java

需要注意的是,虽然聚合根属于实体,但是实体却不只是包含聚合根。事实上,聚合根隶属于实体,同时其内部又可以包含其他实体。举个例子,汽车作为聚合根是一个实体,同时汽车内部的发动机也是一个实体,但发动机却不是聚合根。在本系列的实体和值对象中,我们将详细介绍实体和值对象的区别。

有些对象(特别是聚合根)的创建过程本身也是业务逻辑的一部分,在DDD中,为了显式化业务逻辑,也为了遵从关注点分离的原则,我们将这些对象的构建过程封装到工厂(Factory)中,落地时可以是独立的工厂类,也可以是一个对象中的工厂方法。在码如云中,所有的聚合根对象均配备有专门的工厂类,比如用于创建成员的MemberFactory如下:

@Component
@RequiredArgsConstructor
public class MemberFactory {private final MemberRepository memberRepository;private final DepartmentRepository departmentRepository;public Member create(String name,List<String> departmentIds,String mobile,String email,String password,User user) {//此处只为展示工厂类,省略了具体实现细节return create(name, departmentIds, mobile, email, password, null, user); }
}

源码出处:com/mryqr/core/member/domain/MemberFactory.java

更多关于工厂的讲解,请参考本系列的实体与值对象一文。

在DDD的领域模型中,一个业务操作通常会导致一个结果,这个结果被称为领域事件,即领域模型中已经发生的事情,比如“成员手机号已更新”便是一个领域事件。领域事件通常用于组件之间的因果关系处理,比如当“成员手机号已更新”事件产生后,我们可能会在另一个业务组件中做相应的同步操作,这里的组件粒度可以是聚合根,可以是其他业务模块,还可以是一个独立的第三方系统。

在码如云中,创建成员将产生“成员已创建”事件MemberCreatedEvent

@Getter
@TypeAlias("MEMBER_CREATED_EVENT")
@NoArgsConstructor(access = PRIVATE)
public class MemberCreatedEvent extends DomainEvent {private String memberId;public MemberCreatedEvent(String memberId, User user) {super(MEMBER_CREATED, user);this.memberId = memberId;}}

源码出处:com/mryqr/core/member/domain/event/MemberCreatedEvent.java

更过关于领域事件的讲解,请参考本系列的领域事件一文。

以上的聚合根、实体、值对象、工厂、领域服务和领域事件都是针对领域模型而言的,虽然在DDD中领域模型是当之无愧的大哥大,但是在实际的软件系统中,单单有领域模型是无法正常运作的,还需要有围绕着领域模型的其他周边设施,为此DDD给出了资源库和应用服务等概念。

简单地讲,资源库(Repository)是用于保存/获取聚合根的。在此之前你可能了解过DAO对象也是用于存储对象的,但是与DAO不同的是,资源库操作的基本单位是聚合根,也即只有聚合根对象才配得上拥有资源库,其他实体对象则没有。

在码如云中,成员对象对应的资源库MemberRepository接口定义如下:

public interface MemberRepository {Member byId(String id);boolean exists(String arId);void save(Member member);void delete(Member member);//...此处省略其他方法
}

源码出处:com/mryqr/core/member/domain/MemberRepository.java

一般来说,资源库分为接口类和实现类,接口类属于领域模型,实现类属于基础设施。这样的好处是,领域模型只依赖于接口,而不依赖于基础设施,有利于维持领域模型的技术中立性。更过关于资源库的讲解,请查看本系列的聚合根和资源库一文。

领域模型是用来完成业务功能的,也即需要响应用户发起的各种请求,但是在软件系统中在这些请求到达领域模型之前,事实上还有很多事情需要处理,比如需要从数据库中加载数据(聚合根)、处理事务、权限管控等,在DDD中,这些操作由应用服务(Application Service)完成。应用服务可以看做是领域模型的门面,它将接收到请求派发给合适的领域模型去处理,在整个过程中,应用服务充当的是协调者和编排者的角色,就像酒店的前台一样。

在码如云中,每一个聚合根都有对应的应用服务,比如对于成员来说,应用服务MemberCommandService如下:

//由于应用服务的"Application"与码如云中的应用聚合根"App"重名,在码如云中,使用“CommandService”来表示应用服务,以示区分@Slf4j
@Component
@RequiredArgsConstructor
public class MemberCommandService {//......@Transactionalpublic void changeMyMobile(ChangeMyMobileCommand command, User user) {mryRateLimiter.applyFor(user.getTenantId(), "Member:ChangeMyMobile", 5);String mobile = command.getMobile();verificationCodeChecker.check(mobile, command.getVerification(), CHANGE_MOBILE);Member member = memberRepository.byId(user.getMemberId());memberDomainService.changeMyMobile(member, mobile, command.getPassword());memberRepository.save(member);log.info("Mobile changed by member[{}].", member.getId());}//......
}

源码出处:com/mryqr/core/member/command/MemberCommandService.java

在本系列的应用服务与领域服务一文中,我们将对应用服务做详细讲解。

总结 #

以上,我们概览式地了解了DDD战略设计和战术设计中的各种主要概念,在本系列的后续文章中,我们将针对这些概念进行逐一讲解。

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

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

相关文章

【吴恩达老师《机器学习》】课后习题5之【偏差与方差】

在本练习中&#xff0c;您将实现正则化线性回归&#xff0c;并使用它来研究具有不同偏差-方差特性的模型。 在练习的前半部分&#xff0c;您将实现正则化线性回归&#xff0c;利用水库水位的变化来预测从大坝流出的水量。在后半部分中&#xff0c;您将对调试学习算法进行一些诊…

【C# Programming】值类型、良构类型

值类型 1、值类型 值类型的变量直接包含值。换言之&#xff0c; 变量引用的位置就是值内存中实际存储的位置。 2、引用类型 引用类型的变量存储的是对一个对象实例的引用&#xff08;通常为内存地址)。 复制引用类型的值时&#xff0c;复制的只是引用。这个引用非常小&#xf…

yum和vim工具的使用

目录 yum工具的使用 yum下载原理 软件的查找&下载&删除操作 查找lrzsz软件&#xff08;文件上传或者下载软件&#xff09; 下载lrzsz软件 删除lrzsz软件 vim工具的使用 vim命令模式 命令模式与光标相关的快捷键&#xff1a; 插入模式 底行模式 在本次的博客当中我们主要…

云服务部署:AWS、Azure和GCP比较

&#x1f337;&#x1f341; 博主猫头虎&#xff08;&#x1f405;&#x1f43e;&#xff09;带您 Go to New World✨&#x1f341; &#x1f984; 博客首页——&#x1f405;&#x1f43e;猫头虎的博客&#x1f390; &#x1f433; 《面试题大全专栏》 &#x1f995; 文章图文…

游戏扫码登录+多功能工具箱 微信小程序源码

一个集合了多种实用功能的微信小程序&#xff0c;这个微信小程序源码不仅具备美观的界面设计&#xff0c;还拥有许多实用的功能&#xff0c;而且最重要的是&#xff0c;它无需服务器和域名&#xff0c;所有功能都无需API接口&#xff0c;因此不用担心功能失效的问题。这意味着&…

利用PPT导出一张高清图的方法,office与WPS只需要使用一个即可,我使用的是office。

利用PPT导出一张高清图的方法&#xff0c;office与WPS只需要使用一个即可&#xff0c;我使用的是office。 1&#xff0c;PPT的功能拓展来解决导出高清图片方法1.1&#xff0c;PPT功能拓展—>安装插件&#xff1a; 2&#xff0c;各种方法导出图片效果显示&#xff1a;2.1&…

Linux下git安装及使用

Linux下Git使用 1. git的安装 sudo apt install git安装完&#xff0c;使用git --version查看git版本 2. 配置git git config --global user.name "Your Name“ ##配置用户 git config --global user.email emailexample.com ##配置邮箱git config --global --list …

NLP BigModel

NLP 基础 建议看 [CS224N 2023]打基础 【NLP入门】1. n元语法模型 / 循环神经网络 【NLP入门】3. Word2Vec / GloVe Language Model&#xff1a;语言模型的马尔可夫假设&#xff08;每个词出现的概率仅依赖前面出现的词&#xff09;&#xff0c;是一个自回归模型&#xff08;…

docker部署nginx下日志自动切割方法

前言&#xff1a;nginx采用docker部署&#xff0c;简单方便&#xff0c;但出现一个问题&#xff0c;就是日志没有自动切割&#xff0c;导致access.log 无限增大。如果非docker安装&#xff0c;则nginx的日志默认有切割的&#xff0c;那docker为何没有呢&#xff0c;最后发现&am…

git | 合并merge、衍合rebase与同步更新cherry-pick

git | 合并merge、衍合rebase与同步更新cherry-pick 文章目录 git | 合并merge、衍合rebase与同步更新cherry-pickgit meregegit rebasegit rebase和git merge的区别总结 cherry-pick 把别的分支的改动内容同步更新到自己的分支Example: Reference 欢迎关注公众号“三戒纪元” …

第 4 章 串(文本行编辑实现)

1. 背景说明 该文本编辑器利用串的堆实现&#xff0c;其中对串的原始存储方式基本不作修改&#xff08;有部分修改之处&#xff09;&#xff0c;优化之处在于在串的末尾加上了一个空字符&#xff0c;目的是区分字符串结尾&#xff0c;便于将串保存在文件中&#xff0c;且该优化…

K8S:Pod容器中的存储方式及PV、PVC

文章目录 Pod容器中的存储方式一&#xff0e;emptyDir存储卷1.emptyDir存储卷概念2.emptyDir存储卷示例 二.hostPath存储卷1.hostPath存储卷概念2.hostPath存储卷示例 三.nfs共享存储卷1.nfs共享存储卷示例 四.PV和PVC1.PV、PVC概念2.PVC 的使用逻辑及数据流向3.storageclass插…

公众号迁移多久可以完成?

公众号账号迁移的作用是什么&#xff1f;只能变更主体吗&#xff1f;长期以来&#xff0c;由于部分公众号在注册时&#xff0c;主体不准确的历史原因&#xff0c;或者公众号主体发生合并、分立或业务调整等现实状况&#xff0c;在公众号登记主体不能对应实际运营人的情况下&…

c#:System.Text.Json 的使用三(从Newtonsoft迁移)

环境&#xff1a; .net 6.0vs2022 系列篇&#xff1a; 《c#&#xff1a;System.Text.Json 的使用一》 《c#&#xff1a;System.Text.Json 的使用二》 《c#&#xff1a;System.Text.Json 的使用三&#xff08;从Newtonsoft迁移&#xff09;》 参考&#xff1a; 《MSDN: 从 Newt…

【GO】LGTM_Grafana_gozero_配置trace(4)_代码实操及追踪

最近在尝试用 LGTM 来实现 Go 微服务的可观测性&#xff0c;就顺便整理一下文档。 Tempo 会分为 4 篇文章&#xff1a; Tempo 的架构官网测试实操跑通gin 框架发送 trace 数据到 tempogo-zero 微服务框架发送数据到 tempo 本文就是写一下如何在 go-zero 微服务框架里面配置 t…

C++ list容器的实现及讲解

所需要的基础知识 对C类的基本了解 默认构造函数 操作符重载 this指针 引用 模板等知识具有一定的了解&#xff0c;阅读该文章会很轻松。 链表节点 template<class T>struct list_node{T _data;list_node<T>* _next;list_node<T>* _prev;list_node(const T&…

顺序表的实现和练习

杂谈&#xff1a; 有些数据结构&#xff08;C语言实现&#xff09;的教材/教程中会使用C中引用的语法&#xff0c;引用确实在形式上比指针简洁&#xff0c;这样做无非是为了避免后续对二级指针的使用。 我认为既然使用C语言实现数据结构&#xff0c;那么指针就不应该是门槛。…

关于地址存放的例题

unsigned int a 0x1234; unsigned char b *(unsigned char*)&a; 上面代码大端存储和小端存储的值分别是多少&#xff1f; 大端存储的是把高位地址存放在低位地址处&#xff0c;低位存放到高位。小端是高位存放在高位&#xff0c;低位在低位。因为a是整型&#xff0c;所…

Kafka的消息存储机制

前面咱们简单讲了K啊开发入门相关的概念、架构、特点以及安装启动。 今天咱们来说一下它的消息存储机制。 前言&#xff1a; Kafka通过将消息持久化到磁盘上的日志文件来实现高吞吐量的消息传递。 这种存储机制使得Kafka能够处理大量的消息&#xff0c;并保证消息的可靠性。 1…

vue重修003

文章目录 版权声明day03一、今日目标1.生命周期2.综合案例-小黑记账清单3.工程化开发入门4.综合案例-小兔仙首页 二、Vue生命周期三、Vue生命周期钩子四、生命周期钩子小案例1.在created中发送数据2.在mounted中获取焦点 五、案例-小黑记账清单1.需求图示&#xff1a;2.需求分析…