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

news/2024/5/10 21:31:06

欢迎回来,我是飘渺。今天继续更新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;@Overridepublic 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;}@Overridepublic 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://wed.xjx100/news/301226.html

相关文章

工商业储能解读

工商业储能解读 0、前言1、2022-2023年工商业储能相关利好政策1.1 2022年1月4日1.2 2022年1月18日1.3 2022年2月10日1.4 2022年3月21日1.5 2022年3月22日1.6 2022年3月29日1.7 2022年4月2日1.8 2022年4月13日1.9 2022年4月25日1.10 2022年5月25日1.11 2022年5月30日1.12 2022年…

如何使用ArcGIS制作SketchUp格式三维建筑

GIS数据也可以和传统的三维建模软件进行结合&#xff0c;在很长一段时间内&#xff0c;一直有客户问如何将水经微图中下载的建筑数据转换为SketchUp模型&#xff0c;这里给大家找到了一种解决方案&#xff0c;可以通过插件进行转换&#xff0c;希望能够对你有所帮助。 加载插件…

网络编程与自动化(python)

20.1 网络编程与自动化概述 传统网络运维困境大家在日常的网络运维中是否遇到过如下问题: 设备升级:现网有数千台网络设备,你需要周期性、批量性地对设备进行升级。配置审计:企业年度需要对设备进行配置审计。例如要求所有设备开启sTelnet功能,以太网交换机配置生成树安全…

linux初级阶段性面试题整理(一)

文章目录 网络基础1.OSI七层模型是什么&#xff1f;2.TCP/IP五层模型是什么&#xff1f;3.IPv4的ABC类地址范围4.三种私有网络地址的范围5.常用的TCP端口号及功能6.常用的UDP端口号及其功能7.TCP、UDP协议属于七层模型的哪层&#xff1f;并写出TCP报文段中的三个控制位8.VLAN I…

商品编号篡改测试-业务安全测试实操(7)

商品编号篡改测试,邮箱和用户篡改测试 手机号码篡改测试-业务安全测试实操(6)_luozhonghua2000的博客-CSDN博客 邮箱和用户篡改测试 测试原理和方法 在发送邮件或站内消息时,篡改其中的发件人参数,导致攻击者可以伪造发信人进行钓鱼攻击等操作,这也是一种平行权限绕过漏洞…

Ansys Zemax | 如何在OpticStudio中建模和设计真实波片

本文介绍了如何在 OpticStudio 中建模和设计真实的单色和消色差波片。它将演示如何使用双折射材料&#xff0c;通过构建评价函数来计算相位延迟&#xff0c;并使用 Universal Plot 将相位延迟与波片厚度的关系可视化。&#xff08;联系我们获取文章附件&#xff09; 双折射材料…

git使用方法小结

1.如何git push直接推到远程分支&#xff1f; 先建立本地分支和远程分支的关联 git branch --set-upstream-toorigin/remote_branch your_branch 然后git push 否则就需要git push origin your_branch 2.查看本地版本和远程版本的对应关系 git branch -vv 3.查看本地和远程…

Java低代码开发工具:jvs-rules 2.1.8 新版本功能清单

规则引擎用于管理和执行业务规则。它提供了一个中央化的机制来定义、管理和执行业务规则&#xff0c;以便根据特定条件自动化决策和行为。规则引擎的核心概念是规则。规则由条件和动作组成。条件定义了规则适用的特定情况或规则触发的条件&#xff0c;而动作定义了规则满足时要…