​2:DDD概念大白话

news2024/7/2 3:45:36

产品代码都给你看了,可别再说不会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检查手机号是否重复。

    //领域服务:MemberDomainService

    public 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 {
    
    //......

    @Transactional
    public 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.coloradmin.cn/o/1039064.html

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

相关文章

uni-app打包iOS ipa文件后不上架App store为用户提供下载解决过程记录

写在前面&#xff0c;itms-services协议是什么 itms-services协议是苹果提供的一种让iOS应用在用户设备上无线安装或升级的协议。 具体来说: itms-services表示iOS应用无线安装服务的URL方案,格式为:itms-services://?actiondownload-manifest&urlMANIFEST_URL其中MANIF…

28 WEB漏洞-XSS跨站之WAF绕过及安全修复

目录 常规WAF绕过思路标签语法替换特殊符号干扰提交方式更改垃圾数据溢出加密解密算法结合其他漏洞绕过 自动化工具说明强大的fuzzing引擎安全修复方案演示案例&#xff1a; 常规WAF绕过思路 标签语法替换 xss的效果可以由多个代码来实现&#xff0c;就类似于我们使用到的其它…

02Redis的命令行客户端和桌面客户端的下载和安装

Redis桌面客户端 安装完成Redis服务,我们就可以在Redis的客户端操作Redis的数据库实现数据的CRUD了,客户端分为三类命令行客户端, 图形化桌面客户端,编程客户端 命令行客户端 Redis安装完成后就自带了命令行客户端: redis-cli [options] [commonds] -h选项&#xff1a;指定…

爬虫抓取数据超时是什么原因?如何解决爬虫抓取数据超时问题?

网络爬虫是一种自动化程序&#xff0c;它可以在互联网上抓取数据并将其存储在本地数据库中。然而&#xff0c;有时候&#xff0c;网络爬虫会遇到超时错误&#xff0c;导致无法成功抓取数据。那么&#xff0c;网络爬虫抓取数据显示超时是什么原因呢&#xff1f; 网络连接问题 网…

KMeans算法全面解析与应用案例

目录 一、聚类与KMeans介绍聚类的基础概念KMeans算法的重要性 二、KMeans算法原理数据集和特征空间距离度量算法步骤 三、KMeans案例实战案例背景&#xff1a;客户细分数据集说明Python实现代码输出与解释 四、KMeans的优缺点优点计算效率高算法简单易于实现 缺点需要预设K值对…

8.2 Jmeter if控制器使用

前提:jmeter脚本需要用到if控制器,if判断如果查询不到,则去新增。 1、添加if控制器 线程组-->逻辑控制器-->如果(if)控制器 1)、Expression (must evaluate to true or false) :表达式(值必须是true或false),也就是说,在右边文本框中输入的条件值必须是true 或…

矢量图形编辑软件illustrator 2023 mac软件特点

illustrator 2023 mac是一款矢量图形编辑软件&#xff0c;用于创建和编辑排版、图标、标志、插图和其他类型的矢量图形。 illustrator mac软件特点 矢量图形&#xff1a;illustrator创建的图形是矢量图形&#xff0c;可以无限放大而不失真&#xff0c;这与像素图形编辑软件&am…

表名注解/主键注解/字段注解/乐观锁注解[MyBatis-Plus系列] - 第486篇

悟纤&#xff1a;师傅&#xff0c;脑瓜疼~ 师傅&#xff1a;徒儿这是怎么了&#xff1f; ​ 悟纤&#xff1a;师傅&#xff0c;你了解冷暴力吗&#xff1f; 师傅&#xff1a;略懂略懂。 悟纤&#xff1a;那冷暴力是怎么定义的&#xff1f; 师傅&#xff1a;冷暴力是暴力的一…

华为智能高校出口安全解决方案(2)

本文承接&#xff1a; https://qiuhualin.blog.csdn.net/article/details/131475315?spm1001.2014.3001.5502 重点讲解华为智能高校出口安全解决方案的基础网络安全&业务部署与优化的部署流程。 华为智能高校出口安全解决方案&#xff08;2&#xff09; 课程地址基础网络…

前端web常用的基础案例

html案例&#xff1a; <!DOCTYPE html> <html> <head><title>My Website</title> </head> <body><header><h1>Welcome to My Website</h1><nav><ul><li><a href"#">Home</a…

华为云API对话机器人CBS的魅力—要是有AI,我要做“李白”- 5分钟开发作诗机器人

云服务、API、SDK&#xff0c;调试&#xff0c;查看&#xff0c;我都行 阅读短文您可以学习到&#xff1a;人工智能AI自言语言的情感分析、文本分词、文本翻译 1 IntelliJ IDEA 之API插件介绍 API插件支持 VS Code IDE、IntelliJ IDEA等平台、以及华为云自研 CodeArts IDE&a…

【数据结构】排序合集(万字详解)

文章目录 前言插入排序希尔排序选择排序堆排序快速排序hoare原生版本挖坑法前后指针法三数取中优化随机数取key优化三路划分版非递归 归并排序递归非递归调整边界单次归并单次拷贝 总结 前言 排序&#xff0c;以字面意思来说就是通过特定的算法将一组或多组无序或者接近有序的…

Vue 组件开发总结

Vue 组件开发思路 1. 组件划分 首先&#xff0c;你需要明确定义组件的划分。将大型界面划分为小型、可重用的组件是一个关键步骤。这有助于提高代码的可维护性和可复用性。 2. 组件设计 在设计组件时&#xff0c;考虑组件的输入&#xff08;props&#xff09;和输出&#xf…

Redis_注册为服务

Redis注册服务 1、windowsR ---->services.msc 先查看服务中是否存在redis服务 不存在的话就找到redis解压目录 输入redis-server --service-install&#xff0c;展示如下即为成功 查看服务 此时已经注册成功服务。 卸载服务 使用redis-server --service-uninst…

攻防演练篇 | 企业安全运营之攻防演练——以攻促防

随着互联网技术的发展和企业信息化程度的提高&#xff0c;企业面临的网络安全威胁越来越多。**为了保护企业的信息安全&#xff0c;攻防演练已经成为企业安全运营中不可或缺的一部分。**攻击者通常会利用各种方法来破坏企业的安全系统和数据&#xff0c;因此企业需要像攻击者一…

蓝桥杯 题库 简单 每日十题 day9

01 特殊年份 问题描述 今年是2021年&#xff0c;2021这个数字非常特殊&#xff0c;它的千位和十位相等&#xff0c;个位比百位大1&#xff0c;我们称满足这样条件的年份为特殊年份。输入5个年份&#xff0c;请计算这里面有多少个特殊年份。 输入格式 输入5行&#xff0c;每行一…

高效管理体验?试试docker registry连接

Linux 本地 Docker Registry本地镜像仓库远程连接 文章目录 Linux 本地 Docker Registry本地镜像仓库远程连接1. 部署Docker Registry2. 本地测试推送镜像3. Linux 安装cpolar4. 配置Docker Registry公网访问地址5. 公网远程推送Docker Registry6. 固定Docker Registry公网地址…

C++之list成员函数应用总结(二百三十七)

简介&#xff1a; CSDN博客专家&#xff0c;专注Android/Linux系统&#xff0c;分享多mic语音方案、音视频、编解码等技术&#xff0c;与大家一起成长&#xff01; 优质专栏&#xff1a;Audio工程师进阶系列【原创干货持续更新中……】&#x1f680; 人生格言&#xff1a; 人生…

【pdf密码】打开PDF文件之后发现不能编辑,什么原因?

打开PDF文件的时候&#xff0c;没有提示带有密码&#xff0c;但是打开文件之后发现没有办法编辑PDF文件&#xff0c;这个是因为PDF文件设置了限制编辑&#xff0c;我们需要将限制取消才能够编辑文件。 那么&#xff0c;我们应该如何取消密码&#xff0c;编辑文件呢&#xff1f…

redhat 6.1 测试环境安装 yum

redhat 6.1 测试环境安装 yum 记录 1. 新建虚拟机 1.1 自定义建立虚拟机 自定义创建新的虚拟机 选择硬件兼容性 创建空白硬盘&#xff0c;稍后选择 iso 文件创建系统。 选择操作系统类型 为虚拟机命名 选择处理器配置 选择虚拟机内存 选择虚拟机网络类型 选择…