我正在参与「启航计划」

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

在前面的文章中,咱们深入探讨了DDD的中心概念。我了解,关于初度接触这些概念的你来说,或许难以一次性彻底记住。但别担心,学习DDD并不仅仅是理论的了解,更重要的是将这些理论使用到实践中,了解其设计准则和施行办法。就如同编程界的一句盛行格言所说:“Don’t talk, Show me the Code”。

今日,咱们将以完成用户注册流程为例,一步步展示怎么在实践中使用DDD的设计思维和技术手段,这将有助于你更好地了解并记住DDD的中心概念。让咱们一起开始吧!

手把手教你使用DDD完成用户的注册流程,很优雅!

1. 完成范畴层

在DDD的四层架构中,范畴层扮演着中心人物。因而,咱们首要着手完成这一层,其模块包结构如下:

手把手教你使用DDD完成用户的注册流程,很优雅!

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,暗码有必要进行加密,邮箱格式有必要确保正确),可以自我验证(在结构函数或许工厂办法中验证本身的有效性),而且具有特定的行为(例如暗码的加密和比较)。

关于DP了解,我强烈推荐你阅览这一篇文章。Domain Primitive (qq.com)

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 结构,来简化数据库操作。其模块的包结构如下:

手把手教你使用DDD完成用户的注册流程,很优雅!

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. 完成使用服务层

现在咱们将转向使用服务层的完成。其模块包结构如下:

手把手教你使用DDD完成用户的注册流程,很优雅!

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. 完成用户接口层

最终,咱们来完成用户接口层,作为与外部交互的首要入口。其模块包结构如下:

手把手教你使用DDD完成用户的注册流程,很优雅!

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调用注册接口,用户可以成功注册,暗码也被加密。

手把手教你使用DDD完成用户的注册流程,很优雅!

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

手把手教你使用DDD完成用户的注册流程,很优雅!

6. 小结

本篇文章中,咱们具体地完成了用户注册功用在DDD架构下的设计和完成过程。首要,咱们构建了准确的范畴模型,然后建立根底设施层,完成数据的耐久化。接着,咱们经过使用服务层处理用户注册的恳求与呼应,编列范畴模型的行为。最终,构建了用户接口层,处理HTTP恳求。

值得注意的是,本次实践中咱们并没有选用范畴服务,而是直接在使用服务层处理事务逻辑。这首要是因为注册功用的事务逻辑首要与根底设施层的交互有关,并未涉及到多个范畴模型的协作。但在更杂乱的事务场景中,咱们或许会考虑引入范畴服务。

整体来说,这篇教程旨在帮助你更深入地了解DDD,并将其使用到实践的项目中。未来,咱们将继续优化代码,并评论怎么统一接口层的回来值、处理反常等问题。系列文章,欢迎继续重视。

DDD&微服务系列源码现已上传至GitHub,假如需求获取源码地址,请重视公号 java日知录 并回复关键字DDD即可。