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

发布时间:2025-05-20 03:45:06 作者:益华网络 来源:undefined 浏览量(2) 点赞(2)
摘要:来源:JAVA日知录 欢迎回来,我是飘渺。今天继续更新DDD&微服务的系列文章。 在前面的文章中,我们深入探讨了DDD的核心概念。我理解,对于初次接触这些概念的你来说,可能难以一次性完全记住。但别担心,学习DDD并不仅仅是理论的理解,

来源:JAVA日知录

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

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

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

1. 实现领域层

在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@Builderpublic 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 deliveryAddresses;

    private

 List pointsRecord;

}

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

用户注册时需要提供邮箱、手机号和用户密码,这样在登录时允许使用任何一种方式进行登录数据库不允许使用明文存储密码用户名的长度必须大于等于6

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

1.3  介绍DP

在我们的领域模型中,UserName,CustomerUserPassword,CustomerUserEmail和CustomerUserPhone都被设计为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 框架,来简化数据库操作。其模块的包结构如下:

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 数据库。我们还引入了 mapstruct 和 mapstruct-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

))

@Slf

4j

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. 实现应用服务层

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

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@Validpublic 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

))

@Slf

4j

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. 实现用户接口层

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

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

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

6. 小结

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

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

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

二维码

扫一扫,关注我们

声明:本文由【益华网络】编辑上传发布,转载此文章须经作者同意,并请附上出处【益华网络】及本页链接。如内容、图片有任何版权问题,请联系我们进行处理。

感兴趣吗?

欢迎联系我们,我们愿意为您解答任何有关网站疑难问题!

您身边的【网站建设专家】

搜索千万次不如咨询1次

主营项目:网站建设,手机网站,响应式网站,SEO优化,小程序开发,公众号系统,软件开发等

立即咨询 15368564009
在线客服
嘿,我来帮您!