基于DDD实现的用户注册流程,很优雅!

news2024/11/24 9:25:27

欢迎回来,我是飘渺。今天继续更新DDD&微服务的系列文章。

在前面的文章中,我们深入探讨了DDD的核心概念。我理解,对于初次接触这些概念的你来说,可能难以一次性完全记住。但别担心,学习DDD并不仅仅是理论的理解,更重要的是将这些理论应用到实践中,理解其设计原则和实施方法。就如同编程界的一句流行格言所说:“Don't talk, Show me the Code”。

今天,我们将以实现用户注册流程为例,一步步展示如何在实践中应用DDD的设计思想和技术手段,这将有助于你更好地理解并记住DDD的核心概念。让我们一起开始吧!

d1c98d2980dd94235c9ff0a3ecf9fc95.png

1. 实现领域层

在DDD的四层架构中,领域层扮演着核心角色。因此,我们首先着手实现这一层,其模块包结构如下:

d93033761695126c23e9402064f26688.png

1.1 配置依赖项

<dependencies>
 <dependency>
  <groupId>com.jianzh5</groupId>
  <artifactId>dailymart-common-spring-boot-starter</artifactId>
  <version>${project.version}</version>
 </dependency>
</dependencies>

我们在领域层首先引入了一个通用工具包依赖,这个工具包提供了我们在后续开发中可能需要的一些通用功能。利用这个工具包,我们能够保持代码的整洁,避免在领域层重复编写一些基础功能代码。

1.2 构造领域模型

在第三篇《如何构建商城的领域模型》一文中,我们完成了用户领域对象的建模。其中,最关键的部分是聚合对象CustomerUser

@Data
@Builder
public class CustomerUser {
    private Long customerId;
    private String userName;
    private CustomerUserPassword password;
    private CustomerUserPhone phone;
    private CustomerUserEmail email;
    private Points points;
    private DeliveryAddress defaultAddress;
    private List<DeliveryAddress> deliveryAddresses;
    private List<PointsRecord> pointsRecord;

}

在实现用户注册流程时,我们注意到DailyMart系统对于用户注册活动有几个要求:

  • 用户注册时需要提供邮箱、手机号和用户密码,这样在登录时允许使用任何一种方式进行登录

  • 数据库不允许使用明文存储密码

  • 用户名的长度必须大于等于6

为了满足注册功能的需求,我们对部分属性进行了进一步的抽象,将它们提升为DP(Domain Primitive)对象,这样能够保证它们内在的业务逻辑得到正确的封装。比如,我们将userNamepasswordemailphone都定义为了值对象,并为它们分别定义了合适的业务逻辑。

1.3  介绍DP

在我们的领域模型中,UserNameCustomerUserPasswordCustomerUserEmailCustomerUserPhone都被设计为DP(Domain Primitive)。DP是一个拥有精准定义,自我验证和行为的值对象,它代表了业务领域的最小单元。在实际开发中,我们通常将一些具有业务含义和行为的属性抽象为DP,如此,我们就能够保证这些属性的业务逻辑得到正确的封装和执行。

以CustomerUser对象来说,用户名、密码、邮箱、手机号它们有精准的定义(用户名长度必须>=6,密码必须进行加密,邮箱格式必须保证正确),能够自我验证(在构造函数或者工厂方法中验证自身的有效性),并且拥有特定的行为(例如密码的加密和比较)。

1.4  构建资源库

在DDD中,资源库(Repository)扮演着领域对象持久化的角色,它提供了一种方式,允许我们在不关注底层持久化细节的情况下,实现领域对象的查询和存储。在用户注册功能中,我们创建了CustomerUserRepository资源库接口,并定义了保存用户和按用户名、邮箱、电话查询用户数量的方法。

public interface CustomerUserRepository {
    CustomerUser save(CustomerUser customerUser);

    Long countByUserNameOrEmailOrTelephone(String userName, String email, String phone);
}

在用户注册流程中我们创建了接口CustomerUserRepository,同时提供了两个方法,分别用于保存领域对象和根据条件查询记录条数。

2. 实现基础设施层

接下来,我们将在DailyMart的基础设施层中实现数据持久化。在这里,我们将使用MyBatis-Plus,一款灵活且强大的 ORM 框架,来简化数据库操作。其模块的包结构如下:

e7361846e6cb6bab3549e0209131b0de.png

2.1  配置依赖项

<dependencies>
 ...
 <dependency>
  <groupId>com.baomidou</groupId>
  <artifactId>mybatis-plus-boot-starter</artifactId>
 </dependency>
 <dependency>
  <groupId>mysql</groupId>
  <artifactId>mysql-connector-java</artifactId>
  <scope>runtime</scope>
 </dependency>
 ...
 <dependency>
  <groupId>org.mapstruct</groupId>
  <artifactId>mapstruct</artifactId>
 </dependency>
 <dependency>
  <groupId>org.mapstruct</groupId>
  <artifactId>mapstruct-processor</artifactId>
 </dependency>
</dependencies>

在这段依赖配置中,我们引入了mybatis-plus-boot-starter,这是 MyBatis-Plus 的启动器,用来支持与 Spring Boot 的集成。同时,mysql-connector-java是 MySQL 的 JDBC 驱动,负责连接和操作 MySQL 数据库。我们还引入了 mapstructmapstruct-processor,这是一个用于在 Java 对象之间进行映射转换的工具库,我们将用它来实现领域模型和数据模型的转换。

2.2 构建数据模型

@Data
@TableName("customer_user")
public class CustomerUserDO {
    private Long customerId;
    private String userName;
    private String password;
    private String email;
    private String phone;
    private int points;
}

在这里,我们定义了 CustomerUserDO 类,用于映射数据库的 customer_user 表。它的每个属性都对应数据库表中的一个字段。

2.3 实现模型转换器

@Mapper(componentModel = "spring")
public interface CustomerUserConverter {
    @Mappings({
            @Mapping(target ="points",source = "customerUser.points.value"),
            @Mapping(target = "password",source = "customerUser.password.password"),
            @Mapping(target = "phone",source = "customerUser.phone.phone"),
            @Mapping(target = "email",source = "customerUser.email.email")
    })
    CustomerUserDO domainToDO(CustomerUser customerUser);
}

然后,我们使用 MapStruct 工具库定义了一个转换器接口 CustomerUserConverter,用来实现领域模型 CustomerUser 和数据模型 CustomerUserDO 之间的转换。

2.4 构建数据访问对象

public interface CustomerUserMapper extends BaseMapper<CustomerUserDO> {

}

我们定义了 CustomerUserMapper 接口,继承自 MyBatis-Plus 的 BaseMapper 接口。这样,我们就可以使用 BaseMapper 提供的各种方法来进行数据库操作,大大简化了数据库访问的复杂性。

2.5 实现仓储方法

@Repository
@RequiredArgsConstructor(onConstructor = @__(@Autowired))
@Slf4j
public class CustomerUserRepositoryImpl implements CustomerUserRepository {

    private  final CustomerMapper customerMapper;

    private  final CustomerUserConverter customerUserConverter;

    @Override
    public CustomerUser save(CustomerUser customerUser) {
        CustomerUserDO customerUserDO = customerUserConverter.domainToDO(customerUser);
        int insert = customerMapper.insert(customerUserDO);
        if(insert < 1){
            throw new RuntimeException("用户插入异常");
        }
        Long customerId = customerUserDO.getCustomerId();
        customerUser.setCustomerId(customerId);
        return customerUser;
    }


    @Override
    public Long countByUserNameOrEmailOrTelephone(String userName, String email, String phone) {
        QueryWrapper<CustomerUserDO> queryWrapper = new QueryWrapper<>();
        queryWrapper.or().eq("user_name",userName)
                .or().eq("email",email)
                .or().eq("phone",phone);

        return customerMapper.selectCount(queryWrapper);
    }
}

最后,我们实现了 CustomerUserRepository 接口,这是我们在领域层中定义的用户仓储接口。在实现类 CustomerUserRepositoryImpl 中,我们首先将领域模型转换为数据模型,然后通过 CustomerUserMapper 进行数据库操作。这样,领域模型和数据模型就实现了解耦,领域层和基础设施层之间的交互也变得更加灵活和便捷。

3. 实现应用服务层

现在我们将转向应用服务层的实现。其模块包结构如下:5cdd3750727ac49ee7b817ea6920efb0.png

3.1 配置依赖项

<dependencies>
 ...
 <dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-validation</artifactId>
 </dependency>
</dependencies>

在应用服务层,我们引入了 spring-boot-starter-validation 来对输入参数进行校验。这将保证我们的应用在接收到不符合要求的数据时能够响应适当的错误信息。

3.2 构建数据传输对象(DTO)

@Data
@Valid
public class UserRegistrationDTO {
    @NotBlank(message = "用户名不能为空")
    private String userName;

    @NotBlank(message = "密码不能为空")
    private String password;

    @Email(message = "请输入正确的邮箱格式")
    private String email;

    @NotBlank(message = "手机号不能为空")
    private String phone;
}

我们定义了 UserRegistrationDTO 类,这是一个数据传输对象 (DTO),主要用作接口层和应用层之间传递数据。在这里,它包含了用户注册所需的所有数据,如用户名、密码、电子邮件和手机号。

3.3 构建模型转换器

@Mapper(componentModel = "spring")
public interface CustomerUserAssembler {

    @Mappings({
            @Mapping(target ="password",ignore = true),
            @Mapping(target ="phone",source = "customerUser.phone.phone"),
            @Mapping(target ="email",source = "customerUser.email.email")
    })
    UserRegistrationDTO domainToDTO(CustomerUser customerUser);
}

我们使用了 MapStruct 工具库定义了 CustomerUserAssembler 接口,这是一个转换器,负责将领域模型 CustomerUser 转换为数据传输对象 UserRegistrationDTO

3.4 实现应用服务

@Service
@RequiredArgsConstructor(onConstructor = @__(@Autowired))
@Slf4j
public class CustomerUserService {

    private final CustomerUserRepository customerUserRepository;

    private final CustomerUserAssembler customerUserAssembler;

    public UserRegistrationDTO register(UserRegistrationDTO userRegistrationDTO) {
        // 1. 校验用户是否存在
        boolean exists = existsByUserNameOrEmailOrTelephone(userRegistrationDTO.getUserName(), userRegistrationDTO.getEmail(), userRegistrationDTO.getPhone());

        if(exists){
            throw new RuntimeException("User already exists");
        }

        CustomerUser customerUser = CustomerUser.builder()
                .userName(new CustomerUserName(userRegistrationDTO.getUserName()))
                .phone(new CustomerUserPhone(userRegistrationDTO.getPhone()))
                .email(new CustomerUserEmail(userRegistrationDTO.getEmail()))
                .password(new CustomerUserPassword(userRegistrationDTO.getPassword()))
                .build();

        CustomerUser registerUser = customerUserRepository.save(customerUser);

        return  customerUserAssembler.domainToDTO(registerUser);
    }


    public boolean existsByUserNameOrEmailOrTelephone(String userName, String email, String phone) {
        Long count = customerUserRepository.countByUserNameOrEmailOrTelephone(userName,email,phone);
        log.info("记录条数{}",count);
        return count >= 1;
    }
}

CustomerUserService 类中,我们实现了用户注册的应用服务。首先,我们检查用户是否已经存在;如果不存在,我们将创建一个新的 CustomerUser 并将其保存到仓库。然后,我们将新创建的 CustomerUser 转换为 UserRegistrationDTO,并返回给调用者。

在领域驱动设计 (DDD) 中,我们经常将业务逻辑封装在领域模型中。然而,有些业务逻辑并不适合放在实体或值对象中,如这里的用户名唯一性检查,因为这需要与用户仓库进行交互,这是一个涉及基础设施的操作。领域模型应尽可能地与基础设施保持解耦,所以这样的业务逻辑更适合放在服务层中。

4. 实现用户接口层

最后,我们来实现用户接口层,作为与外部交互的主要入口。其模块包结构如下:

40b456d0bba18cd6dd7163ec445dc74a.png

4.1 配置依赖

为了使用应用服务层的功能,我们需要添加其依赖项:

<dependencies>
 <dependency>
  <groupId>com.jianzh5</groupId>
  <artifactId>dailymart-customer-application</artifactId>
  <version>${project.version}</version>
 </dependency>
</dependencies>

4.1 构建注册接口

接下来,我们构建用户注册的RESTful接口。该接口将接收一个UserRegistrationDTO对象作为参数,并调用服务层的register方法进行用户注册。

@RestController
@RequiredArgsConstructor(onConstructor = @__(@Autowired))
public class CustomerController {

    private final CustomerUserService customerService;

    @PostMapping("/api/customer/register")
    public UserRegistrationDTO register(@RequestBody @Valid UserRegistrationDTO customerDTO){
        return customerService.register(customerDTO);
    }
}

4.2 配置启动类

最后,我们需要配置应用的启动类,它将启动整个Spring Boot应用并扫描指定包中的Mapper接口。

@SpringBootApplication
@MapperScan("com.jianzh5.dailymart.module.customer.infrastructure.dao.mapper")
public class CustomerUserApplication {
    public static void main(String[] args) {
        SpringApplication.run(CustomerUserApplication.class,args);
    }
}

4.3 测试验证

完成了上述工作,就可以进行测试验证了。

下面我用postman调用注册接口,用户可以成功注册,密码也被加密。

0aadb72e7d16fdcdee202d705d8d427a.png

当使用相同的用户名、手机号、邮箱注册时,后台日志会提示用户已存在的异常。

aeb9400c4c26b1a74c63295d53f1b5ef.png

6. 小结

本篇文章中,我们详细地实现了用户注册功能在DDD架构下的设计和实现过程。首先,我们构建了精确的领域模型,然后建立基础设施层,实现数据的持久化。接着,我们通过应用服务层处理用户注册的请求与响应,编排领域模型的行为。最后,构建了用户接口层,处理HTTP请求。

值得注意的是,本次实践中我们并没有采用领域服务,而是直接在应用服务层处理业务逻辑。这主要是因为注册功能的业务逻辑主要与基础设施层的交互有关,并未涉及到多个领域模型的协作。但在更复杂的业务场景中,我们可能会考虑引入领域服务。

总体来说,这篇教程旨在帮助你更深入地理解DDD,并将其应用到实际的项目中。未来,我们将继续优化代码,并讨论如何统一接口层的返回值、处理异常等问题。系列文章,欢迎持续关注。

··············  END  ··············

最后,欢迎关注公众号加入知识星球,获取最新的文章和源码更新。在公众号回复关键词“知识星球”,获取限量30元优惠券,每天仅需不到3毛钱。

1cdbe0ea30244385d9a441e6d4b2850b.jpeg

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

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

相关文章

6.SpringCloudAlibaba 整合 Sentinel

一、分布式系统遇到的问题 1 服务雪崩效应 在分布式系统中&#xff0c;由于网络原因或自身的原因&#xff0c;服务一般无法保证 100%是可用的。如果一个服务出现了问题&#xff0c;调用这个服务就会出现线程阻塞的情况&#xff0c;此时若有大量的请求涌入&#xff0c;就会出现…

4.11 socket地址 4.12 IP地址转换函数 4.13TCP通信流程 4.14socket函数

4.11 socket地址 socket地址其实是一个结构体&#xff0c;封装端口号和IP等信息。后面的socket相关的api中需要使用到这个socket地址。 客户端 -> 服务器&#xff08;IP, Port&#xff09; 通用 socket地址 socket 网络编程接口中表示 socket 地址的是结构体 sockaddr&am…

安装Vue(重点笔记)

目录 什么是Vue? 特点 Node.js安装 Vue安装 1、安装Vue.js 1.1&#xff09;安装失败解决 1.2&#xff09;安装成功 2、安装webpack模板 3、安装脚手架 4、安装vue-router 创建第一个Vue-cli应用程序 1、命令行(cmd) cd 到指定的目录 2. 创建第一个基于webpack模…

【C#】WinForm中如何获取一个控件相对于主界面的位置

文章目录 前言一、新建WinForm程序二、效果与代码总结 前言 使用button控件的 PointToScreen 方法和Form控件的 PointToClient 方法来获取button1相对于Form边界的位置。具体步骤如下&#xff1a; 获取button1在屏幕上的位置&#xff1a; Point button1ScreenPos button1.P…

【TensorRT】TensorRT的环境配置

本文主要记录TensorRT8.6的环境配置过程&#xff01; 官方文档&#xff1a;NVIDIA TensorRT - NVIDIA Docs TensorRT相关版本的文档&#xff1a; Documentation Archives :: NVIDIA Deep Learning TensorRT Documentation 一 、下载CUDA和cudann CUDA下载&#xff1a;CUDA T…

最近不知道写啥,打算整理点儿关于钱币的文章,也转载点儿别人的技术文章,毕竟,洒家还是干技术滴嘛...

最近开始整理之前陆陆续续买的乱七八糟的东西了&#xff0c;现在一堆东西还乱扔着呢~~ 先整理了一套大五帝钱还有下面这套清五帝。 清五帝钱一套: 27宝泉局顺治通宝&#xff0c;铁壳锈 27.5康熙通宝满汉同 26.5宝泉局雍正通宝 三离划 26&#xff08;卡&#xff09;宝云局乾隆通…

Vision Transformer综述 总篇

Vision Transformer综述 1. Transformer简介2. Transformer组成2.1 Self-AttentionMulti-Head Attention&#xff08;多头注意力&#xff09; 2.2 Transformer的其他关键概念2.2.1 Feed-Forward Network 前馈网络2.2.2 Residual Connection 残差连接2.2.3 解码器中的最后一层 3…

Go GC:了解便利背后的开销

1. 简介 当今&#xff0c;移动互联网和人工智能的快(越)速(来)发(越)展(卷)&#xff0c;对编程语言的高效性和便利性提出了更高的要求。Go作为一门高效、简洁、易于学习的编程语言&#xff0c;受到了越来越多开发者的青睐。 Go语言的垃圾回收机制&#xff08;Garbage Collectio…

client-go的Indexer三部曲之一:基本功能

关于《client-go的Indexer三部曲》系列 该系列是《client-go实战系列》的子系列文章&#xff0c;共三篇内容&#xff0c;分别从功能、性能、源码三个角度对client-go内部的Indexer组件进行说明&#xff0c;目标是与大家一同学习Indexer&#xff0c;并掌握如何在开发中通过Inde…

EBU5476 Microprocessor System Design 知识点总结_6 Serial Communication

Serial Communication 串口通信&#xff0c;一种发送消息的通信方式。 串&#xff0c;指的是发数据的方式&#xff1a;一位一位串行发&#xff0c;并行是可能有多路通道&#xff0c;每路同时发一个数据&#xff0c;多路同时到达。 串口通信有单工 Simplex&#xff0c;半双工…

基于SpringBoot+Vue的乐校园二手书交易管理系统设计与实现

博主介绍&#xff1a; 大家好&#xff0c;我是一名在Java圈混迹十余年的程序员&#xff0c;精通Java编程语言&#xff0c;同时也熟练掌握微信小程序、Python和Android等技术&#xff0c;能够为大家提供全方位的技术支持和交流。 我擅长在JavaWeb、SSH、SSM、SpringBoot等框架下…

35. 应用监控【监控端点健康信息】

1、展示健康信息详情 开发者可以通过查看健康信息来获取应用的运行数据&#xff0c;进而提早发现应用问题&#xff0c;提早解决&#xff0c; 免造成损失。默认情况下开发者只能获取 status 信息&#xff08;见图 1 &#xff09;&#xff0c;这是因为 detail 信息默认不显示&…

【Java常见面试题】Spring篇

导航&#xff1a; 【黑马Java笔记踩坑汇总】JavaSEJavaWebSSMSpringBoot瑞吉外卖SpringCloud黑马旅游谷粒商城学成在线常见面试题 目录 1、简单介绍Spring 2、说说你对IOC的理解 3、说说你对AOP的理解 4、说说Bean的生命周期 5、说说循环依赖和三级缓存 6、说说Bean的几种…

DataV图表-排名轮播表自定义

DataV图表-排名轮播表自定义数据大屏可视化 场景&#xff1a;需要计算根据分数不同柱子的颜色不同 低于60分变成为橙色柱子 一开始使用的是 dv-scroll-ranking-board 这个不可以自定义颜色和属性 我们可以更改 dv-scroll-board 样式来实现 排名轮播表 安装 data-view npm ins…

如何使用Leangoo领歌管理敏捷缺陷

缺陷管理通常关注如下几个方面&#xff1a; 1. 缺陷的处理速度 2. 缺陷处理的状态 3. 缺陷的分布 4. 缺陷产生的原因 使用​​​​​​​Leangoo领歌敏捷工具​​​​​​​我们可以对缺陷进行可视化的管理&#xff0c;方便我们对缺陷的处理进展、负责人、当前状态、分布情…

『 MySQL篇 』:MySQL 锁机制介绍

目录 一. 概述 二. 全局锁 三 . 表级锁 三. 行级锁 一. 概述 锁是计算机协调多个进程或线程并发访问某一资源的机制。在数据库中&#xff0c;除传统的计算资源&#xff08;CPU、RAM、I/O&#xff09;的争用以外&#xff0c;数据也是一种供许多用户共享的资源。如何保证数据…

三菱FX5U通讯、定位、伺服32讲

三菱FX5U系列&#xff08;现在已经升级改成为MELSEC iQ-F系列&#xff09;PLC的CPU模块中内置了能够支持各种控制的优异功能&#xff0c;全系标配Ethernet端口、RS-485端口、SD存储卡槽。Ethernet端口可支持CC-Link IE现场网络Basic&#xff0c;因此能连接多种多样的设备。 第一…

eChart折线图动态特效和隔几秒高亮特效

示例&#xff1a; 说明&#xff1a; 因为现在公司经常要做大屏可视化特效&#xff0c;没办法&#xff0c;只能让图尽量动起来&#xff08;之前开会挨叼了&#xff0c;说俺们深圳做的&#xff0c;不能比西安那些人做的差。。。&#xff09; 主要代码&#xff1a; 折线图的滚呀滚…

基于Hexo和Butterfly创建个人技术博客,(7) 配置butterfly主题搭建博客网站主体UI框架

Butterfly官方网站&#xff0c;请 点击进入。 本文面向使用 butterfly theme 的用户, 主题安装方法可查看基于Hexo和Butterfly创建个人技术博客&#xff0c;(1) 初始化博客站点 这章内容。 一、概述 1、什么是theme? Hexo可以认为是一个基础框架&#xff0c;主要提供渲染和插…

平行云——开启通往元宇宙的通道

元宇宙是平行于真实世界的虚拟世界&#xff0c;是新一代互联网。具有真三维、可交互、可沉浸特性的XR&#xff0c;是构建元宇宙的终极数字媒体形态。如何打破XR终端设备与XR内容之间的紧耦合&#xff0c;实现任意平台、任意终端的线上访问&#xff0c;Cloud XR是其必由之路&…