产品代码都给你看了,可别再说不会DDD(一):DDD入门

这篇具有很好参考价值的文章主要介绍了产品代码都给你看了,可别再说不会DDD(一):DDD入门。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

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

产品代码都给你看了,可别再说不会DDD(一):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等。

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

码如云源代码:https://github.com/mryqr-com/mry-backend

DDD入门

本文是本系列的第一篇文章,主要讲解DDD入门知识,如果你已经对DDD有所了解,可跳过本文。

在阅读本文之前,你可能会认为DDD是整天做PPT的架构师们才应该去关注的东西;或者会认为DDD是比较顶层的东西,跟我写代码的程序员关系不大;你可能还会认为DDD是一种被咨询师们吹得天花乱坠但是却无法落地的概念炒作而已。在日常实践中,我接触过不懂装懂的言必称DDD者,也见识过声称DDD与编码毫无关系的虚无主义者,当然也接触过真正能将DDD落地者。在本系列文章中,我将向你证明,DDD正是软件工程师的工具,可以用于编写更好的代码,设计更好的架构,进而做出更好的软件。当然,我也会针对DDD中被夸大其词的那部分进行澄清,甚至批评。

DDD是什么呢?是架构思想?是方法论?还是软件之道?从某种层度上说这些都对,但是对于程序员或者架构师来讲,最接地气的回答应该是:DDD是面向对象进阶。对于写了几年代码希望在职业生涯中更上一层楼的程序员来说,学习DDD是再适合不过的了。为了能让DDD新手们更快地上手,我们还是以代码为入口展开讲解,首先让我们来看看DDD项目代码和非DDD项目代码有何不同。

实现业务逻辑的三种方式

在案例项目码如云中有这样一个业务需求:所有可登录的用户被称为成员(Member),成员可以自行修改自己的手机号码,修改后该成员将被标记为“手机号已识别”的状态。为了实现这个需求,我们分别通过三种方式予以实现,读者可以对照看看这些实现方式是不是和自己曾经的编码方式有相似之处。

第一种: 事务脚本

对于上述需求,从纯技术上讲,我们希望达到的最终目的不过是在数据库中的member表中更新2个字段而已,一个是手机号(mobile_number)字段,另一个是手机号已识别(mobile_identified)字段。为了实现这个需求,最简单直接的方式难道不是直接写个SQL语句直接更新数据库表么?的确如此,这个简单的方式其实有个专门的名词 —— 事务脚本(Transactional Script),也即通过类似编写脚本的方式完成一个业务用例,一个业务用例对应一次事务。

    @Transactional//事务边界
    public void updateMyMobile(String mobileNumber, String memberId) {
        
        //采用事务脚本的方式,直接通过SQL语句实现业务逻辑
        String sql = "update member set mobile_number = ? , mobile_identified = 1 where id = ?;";
        jdbcTemplate.update(sql, mobileNumber,memberId);
    }

这种直接通过技术手段实现业务功能的方式没有任何软件建模可言,它将原本可以分开的业务性代码和技术性代码揉杂在一起,既不利于业务的重用,也不利于系统的长期演进,因此通常被认为只适合一些小型软件项目。

第二种:贫血对象

看到第一种实现方式你可能会想:这都什么年代了,还在像写C语言那样编写代码,不使用点儿面向对象技术连一个刚入职的毕业生估计都不好意思。那好吧,让我们创建一个Member对象。

    @Transactional
    public void updateMyMobile(String mobileNumber) {
        String memberId = CurrentUserContext.getCurrentMemberId();
        Member member = memberRepository.findMemberById(memberId);

        //先后调用Member对象中的2个setter方法实现业务逻辑
        member.setMobileNumber(mobileNumber);
        member.setMobileIdentified(true);

        memberRepository.updateMember(member);
    }

在上例中,首先我们将数据库访问相关的逻辑全部封装在memberRepository中,从而解决了“技术性代码和业务性代码揉杂”的问题。其次,创建了Member对象,其中包含两个setter方法,setMobileNumber()用于设置手机号码,setMobileIdentified()用于标记标记手机号已识别,这应该面向对象了吧?!但是,问题恰恰出在了这两个setter方法上:此时的Member对象只是一个数据容器而已,而非真正的对象。这种只有数据没有行为的对象被称为贫血对象。

问题还不止于此,本例中先后调用的两个setter方法事实上违背了软件开发的一个根本性原则 —— 内聚性。简单来讲,“设置手机号”和“标记手机号已识别”这两个步骤在业务上是紧密联系在一起的,应该由Member中的单个方法完成,而不应该由2个独立的方法完成。为了解释这里体现的内聚性,让我们再来看个需求:除了成员自己可以修改手机号外,管理员也可以为任何成员设置手机号,为此我们再实现一个updateMemberMobile()方法。

    @Transactional
    public void updateMemberMobile(String mobileNumber,String memberId) {
        Member member = memberRepository.findMemberById(memberId);

        //与updateMyMobile()相同,需要先后调用Member对象中的2个setter方法实现业务逻辑
        member.setMobileNumber(mobileNumber);
        member.setMobileIdentified(true);

        memberRepository.updateMember(member);
    }

这里,updateMemberMobile()方法也需要显式地先后调用Member的setMobileNumber()setMobileIdentified()方法,也就是说编码者需要记住必须同时调用2个方法,否则程序就会出Bug。这种方式存在以下问题:

  1. 业务逻辑的泄漏:对于维持“设置手机号”和“标记手机号已识别”同时发生的职责来说,本应该由Member对象自身完成的,结果泄漏到了Member对象的外部;
  2. 增加调用者的负担:对于作为Member客户方的updateMyMobile()updateMemberMobile()方法来讲,他们本应该将Member当做一个黑盒,但在本例中却需要了解Member的内部细节(先后调用setMobileNumber()setMobileIdentified()方法),这无疑是调用者的负担。
  3. 难于维护:如果以后业务需求有变,那么需要同时修改updateMyMobile()updateMemberMobile()2个方法,这可能不是能够轻易做到的,特别是在人员流动频繁的软件项目中。

与事务脚本相似,贫血对象除了可用于一些小的软件项目外,通常被认为是一种反模式,应该避免使用。

第三种:领域对象

领域对象是一个与贫血对象相对立的概念,它表示直接体现业务逻辑的一类对象,这类对象不仅包含业务数据,还包含业务行为。领域对象希望达到的理想状态是:所有业务逻辑均由领域对象完成,外界将领域对象当做一个黑盒向其发送指令(调用方法)即可。在本例中,设置手机号的同时需要标记“手机号已识别”均属业务逻辑,应该全部放到领域对象中完成。

    @Transactional
    public void updateMyMobile(String mobileNumber) {
        String memberId = CurrentUserContext.getCurrentMemberId();
        Member member = memberRepository.findMemberById(memberId);

        //只需调用Member种的updateMobile()方法即可
        member.updateMobile(mobileNumber);

        memberRepository.updateMember(member);
    }

这里,updateMyMobile()方法只需调用Member中的updateMobile()方法即可,然后由Member自行处理具体的业务逻辑:

    //由Member对象自身处理同时更新mobileNumber和mobileIdentified字段
    public void updateMobile(String mobileNumber) {
        this.mobileNumber = mobileNumber;
        this.mobileIdentified = true;
    }

在本例中,除了将数据和行为同时放到Member对象之外,我们还会考虑如何设计和安排这些行为才最得当,比如将高内聚的mobileNumbermobileIdentified放到同一个方法中,此时的Member便是一个行为饱满的领域对象,并开始变得有些“领域驱动”的意味了,所谓的"DDD是面向对象进阶"这个说法也正体现于此。事实上,在DDD中Member对象也被称为聚合根,而“更新mobileNumber的同时需要一并更新mobileIdentified”则被称为聚合根的不变条件,我们将在后续文章中对此做详细讲解。

看到这里,你可能会问:领域对象的实现方式不就是将贫血对象中的业务逻辑实现挪了个位置吗?的确,但是这一挪,便挪出了编程的讲究与思考,挪出了模型的设计与原则,挪出了软件的发展与进步。就像云计算早年被认为不过是将本地的计算资源搬移到网络上一样,我们将很多看似并不具有颠覆性的微小创新合在一起,便可将理想编织成一个个能够为行业为社会带来实际进步的美好现实。

你可能还会说,领域对象这种实现方式我平时就是这么做的呀!?没错,我们平时编程的很多做法其实已经包含了DDD中的某些思想或实践,因为DDD并不是什么全新的东西要把你所写的代码全部推翻重来,而是很多具有逻辑归因性的东西其实大家都能总结出来,只是那些大牛总结得比我们更早,更系统,更全面而已。

对于以上三种实现方式,我们在前面提到事务脚本和贫血对象只适合一些小型的软件项目,那么问题来了,到底多小才算小呢?这个问题没有标准答案,就像你问微服务多小算小一样,It depends!然而,但凡是企业中立过项的软件项目,都不会是实现一个Code Kata这么简单,都不能被定义为“小型项目”。因此,对于几乎所有企业级软件系统来说,使用领域对象进而DDD都不会是个错误的选择。

真实产品代码

由于本文是入门性质的文章,故到目前为止所使用的代码均不是码如云的产品代码。接下来,让我们来看看真实的产品代码,对于“成员修改自己的手机号”的业务功能,码如云代码库中的实现如下:

    @Transactional
    public void changeMyMobile(ChangeMyMobileCommand command, User user) {
        //API限流器,与DDD无关,读者可忽略
        mryRateLimiter.applyFor(user.getTenantId(), "Member:ChangeMyMobile", 5);

        //将所有请求相关的数据封装到Command对象中
        String mobile = command.getMobile();

        //修改手机号时,需要验证发往新手机号的验证码
        verificationCodeChecker.check(mobile, command.getVerification(), CHANGE_MOBILE);

        Member member = memberRepository.byId(user.getMemberId());

        //这里调用了MemberDomainService中的方法,而不是直接调用Member,因为需要检查手机号是否重复,而Member自身无法完成该检查
        memberDomainService.changeMyMobile(member, mobile, command.getPassword());

        memberRepository.save(member);
        log.info("Mobile changed by member[{}].", member.getId());
    }

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

为了让读者能对代码有更加详尽的了解,我们在源代码中加上了注释,建议读者通过阅读这些注释来理解代码的意图。(真实的码如云代码库中是很少有注释的,因为我们坚持“代码即是设计”的原则,让代码本身直接体现业务意图)

在本例中,首先使用限流器MryRateLimiter对请求进行限流处理,然后使用VerificationCodeChecker对手机号验证码进行检查,最后才调用MemberDomainService完成实际的业务逻辑。你可能有些纳闷儿,为什么不像前文中那样直接调用Member对象中的方法,而是调用MemberDomainService呢?事实上,这里的MemberDomainService在DDD中被称为领域服务,用于处理领域对象自身无法处理的业务逻辑。在本例中,成员在修改手机号时,系统需要检查该手机号是否已经被其他成员所占用,这部分逻辑是无法通过单个Member自身完成的,只能通过一个可以跨多个MemberMemberDomainService完成。

对于诸如限流器MryRateLimiter这些与DDD无关的代码,我们将在后续文章的代码中予以删除,以使代码集中在对DDD的阐述上。

MemberDomainService.changeMyMobile()方法实现如下:

    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对象中的方法,完成对手机号的修改
        member.changeMobile(newMobile, member.toUser());
    }

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

可以看到,MemberDomainService调用了MemberRepository.existsByMobile()用于检查手机号是否已经被占用,如果是,则抛出异常。

最后,MemberDomainService调用Member.changeMobile()方法完成对手机号的修改:

public void changeMobile(String mobile, User user) {
        if (Objects.equals(this.mobile, mobile)) {
            return;
        }

        //同时设置mobile字段和mobileIdentified的值,高度内聚
        this.mobile = mobile;
        this.mobileIdentified = true;
        
        this.addOpsLog("修改手机号为[" + mobile + "]", user);
    }

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

如前文所述,mobilemobileIdentified是高度内聚的,因此放在Member的同一个方法changeMobile()中完成更新。以后,无论通过什么业务渠道修改成员的手机号,都只需要调用相同的Member.changeMobile()方法即可。

DDD书籍推荐

我基本上参阅完了市面上所有的DDD书籍(截止到2023年3月份),在这些书籍中,真正值得推崇的有以下4本书:

  • 《领域驱动设计:软件核心复杂性应对之道》(蓝皮书,从左往右第一本,首版时间2003年):DDD的开山之作,对于初学者来说阅读起来有些晦涩,不建议初学者直接阅读该书
  • 《实现领域驱动设计》(红皮书,从左往右第二本,首版时间2013年):这本是讲DDD落地的经典书籍,其中包含大量代码示例,很多人都是通过这本书才真正进入DDD的世界
  • 《领域驱动设计模式、原理与实践》(从左往右第三本,首版时间2015年):这也是一本能够帮你系统的完成DDD落地的书籍
  • 《解构领域驱动设计》(首版时间2021年):国内第一本关于DDD的专著,作者张逸在DDD社区具有比较大的影响力

对于英文书籍,建议大家如果有条件的话,一定阅读英文原版,因为那才是第一手资料,中文翻译始终存在漏译错译等无法表达原书本意的情况。

总结

本文从事务脚本、贫血对象和领域对象三种实现业务逻辑的方式为入口,一步一步地引入DDD的概念,希望能让DDD新手们平滑地开启DDD的学习之路。在下一篇:DDD概念大白话文章中,我们将通过大白话的方式给大家讲解DDD中的各种概念,以让读者对DDD有个全景式的认识。文章来源地址https://www.toymoban.com/news/detail-642057.html

到了这里,关于产品代码都给你看了,可别再说不会DDD(一):DDD入门的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • Selenium+Unittest自动化测试框架实战(框架源码都给你)

    目录 前言 项目框架 首先管理时间 !/usr/bin/env python3 -- coding:utf-8 -- 配置文件 conf.py config.ini# 读取配置文件 记录操作日志 简单理解POM模型 管理页面元素 封装Selenium基类 创建页面对象 熟悉unittest测试框架 编写测试用例 执行用例 生成测试报告 执行并生成报告 发送结果邮件 se

    2024年02月15日
    浏览(44)
  • 【跳槽面试】一份tcp、http面试指南,常考点都给你了

    要说http就绕不开tcp,TCP协议对应于传输层,而HTTP协议对应于应用层,从本质上来说,二者没有可比性。但是,http是基于tcp协议的。 TCP/IP 协议分层模型 物理层将二进制的0和1和电压高低,光的闪灭和电波的强弱信号进行转换 链路层代表驱动 网络层 使用 IP 协议,IP 协议基于

    2024年01月23日
    浏览(65)
  • 记录--求你了,别再说不会JSONP了

    JSONP是一种很远古用来解决跨域问题的技术,当然现在实际工作当中很少用到该技术了,但是很多同学在找工作面试过程中还是经常被问到,本文将带您深入了解JSONP的工作原理、使用场景及安全注意事项,让您轻松掌握JSONP。 JSONP,全称JSON with Padding,是一项用于在不同域之

    2024年02月05日
    浏览(39)
  • LAL v0.36.7发布,Customize Sub,我有的都给你

    Go语言流媒体开源项目 LAL 今天发布了v0.36.7版本。 LAL 项目地址:https://github.com/q191201771/lal 老规矩,简单介绍一下: ▦ Customize Sub,我有的都给你 这是提供给用lalserver做二次开发的小伙伴们的一个重要的功能,业务方可以通过设置回调函数的方式获取lalserver内部的流。 获取到

    2024年02月16日
    浏览(46)
  • 不要再说你不会了——网络性能问题排查思路

    服务监控系列文章 服务监控系列视频 网络问题往往是性能排查中最复杂的一个问题,因为网络问题往往涉及的链路比较长,排查起来不仅仅是看本地机器的指标就可以了。本文将展示一个比较系统的排查网络问题的思路。 我们往往都是通过类似prometheus,grafana搭建的监控平

    2023年04月13日
    浏览(35)
  • 当面试问你接口测试时,不要再说不会了

    很多人会谈论接口测试。到底什么是接口测试?如何进行接口测试?这篇文章会帮到你。 在谈论接口测试之前,让我们先明确前端和后端这两个概念。 前端是我们在网页或移动应用程序中看到的页面,它由 HTML 和 CSS 编写而成,让我们看到漂亮的页面,并进行一些简单的校验

    2024年02月08日
    浏览(47)
  • jmeter接口测试项目实战详解,零基础也能学,源码框架都给你

    目录 1.什么是jmeter? 2.jmeter能做什么? 3.jmeter环境搭建 3.1前提: 3.2jmeter下载: 3.3jmeter环境搭建: 3.3.1mac当中jmeter环境搭建: 3.4jmeter基本配置 3.4.1.切换语言  3.4.2.安装插件 4.jmeter组件 4.1测试计划 4.2线程组 4.2.1取样器错误后要执⾏的动作 4.2.2线程属性 4.3jmeter监听器 4.3.1聚合

    2024年02月08日
    浏览(43)
  • 软件测试项目去哪里找?我都给你整理好了【源码+操作视频】

    目录 一、引言 二、测试任务 三、测试进度 四、测试资源 五、测试策略 六、测试完成标准 七、风险和约束 八、问题严重程度描述和响应时间规范 九、测试的主要角色和职责 ​有需要实战项目的评论区留言吧! 软件测试是使用人工或者自动的手段来运行或者测定某个软件

    2024年02月07日
    浏览(51)
  • Selenium中操作iframe,别再说你不会了

    📢专注于分享软件测试干货内容,欢迎点赞 👍 收藏 ⭐留言 📝 如有错误敬请指正! 📢交流讨论:欢迎加入我们一起学习! 📢资源分享:耗时200+小时精选的「软件测试」资料包 📢 软件测试学习教程推荐:火遍全网的《软件测试》教程 这里是清安,本章一起来了解一下

    2024年02月03日
    浏览(38)
  • 大佬都是怎么画交换机拓扑图的?都给你整理好了

    老杨的网工交流群里经常会有这种现象: 一群小伙伴在问各类型拓扑图的问题,怎么设计,怎么配置,或者让群里的大佬帮忙看看,这图有没有啥问题的…… 太多了。 网络拓扑(Network Topology)是啥?你可以把他理解成是用传输媒体互连各种设备的物理布局。 为了更好的连

    2024年02月09日
    浏览(39)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包