如何使用 DDD搭建工程?手把手带你落地!

大家好,我是猿java

DDD是微服务中经常用到的一种架构方式,在实际工作中,我们该如何快速落地一个 DDD工程呢?这篇文章,我们将手把手带你落地一个 DDD项目,不管你有没有 DDD经验,都可以轻松使用。

在开始我们的文章之前,我们还是要简单的了解下 DDD是什么,帮助我们下面更好地理解代码工程。

1. 什么是DDD?

DDD,全称 Domain-Driven Design,翻译为领域驱动设计,它是一种软件开发方法论,由埃里克·埃文斯(Eric Evans) 在其2003年出版的同名书籍中提出。DDD旨在通过密切关注复杂软件系统的核心业务领域,将业务需求与技术实现紧密结合,从而提高软件的可维护性、可扩展性和灵活性。

1.1 DDD 的核心理念

DDD 以领域为核心,强调将业务领域作为软件开发的核心,致力于深入理解业务需求和业务规则,通过建模来反映实际业务问题。

1.2 统一语言

DDD使得开发团队和业务专家共同使用的一种准确、一致的语言(Ubiquitous Language),用于描述业务领域中的概念、流程和规则,减少沟通障碍,提高理解一致性。

1.3 战略设计与战术设计

DDD 分为战略设计和战术设计两部分:

  • 战略设计:关注整个系统的高层次结构和模块划分,定义不同的子域(Subdomain)和上下文边界(Bounded Context)。
  • 战术设计:关注特定上下文内的细节,实现领域模型和相关组件。

说实话,DDD的理论确实很烧脑,我们会在后续的文章中慢慢拆解。不管怎样,在对 DDD有了简单的了解之后,我们要进入今天的核心部分:DDD代码实操。

本文目标:使用DDD + SpringBoot + JPA + 双数据源(MySQL + DynamoDB)实现对 user表进行添加和查询功能,完全适合小白操作。

2. 项目整体结构

首先,我们先看下整个工程建的主要模块以及模块之间的依赖关系:

  1. domain:核心领域模型和业务逻辑。
  2. repository:仓储接口定义。
  3. application:应用服务层,协调领域对象和仓储。
  4. infrastructure:基础设施层,包括具体的仓储实现(MySQL 和 DynamoDB)、配置等。
  5. config:独立的配置模块(可选,视项目复杂程度而定)。
  6. api(或 web):入口层,如 REST API 控制器,主要处理外部接口请求。

模块结构如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
ddd-project/
├── build.gradle
├── settings.gradle
├── domain/
│ └── build.gradle
├── repository/
│ └── build.gradle
├── application/
│ └── build.gradle
├── infrastructure/
│ ├── build.gradle
│ ├── persistence-mysql/
│ │ └── build.gradle
│ └── persistence-dynamodb/
│ └── build.gradle
├── config/
│ └── build.gradle
└── api/
└── build.gradle

3. 项目模块详解

3.1 Gradle 配置

3.1.1 根项目 settings.gradle

在根项目的 settings.gradle 中,包含所有子模块:

1
2
3
4
5
6
7
8
9
10
rootProject.name = 'ddd-project'

include 'domain'
include 'repository'
include 'application'
include 'infrastructure'
include 'infrastructure:persistence-mysql'
include 'infrastructure:persistence-dynamodb'
include 'config'
include 'api'

3.1.2 根项目 build.gradle

根项目的 build.gradle 通常用于定义全局的插件和依赖管理。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
plugins {
id 'java'
id 'org.springframework.boot' version '3.0.0' apply false
id 'io.spring.dependency-management' version '1.1.0' apply false
}

allprojects {
group = 'com.example'
version = '1.0.0'

repositories {
mavenCentral()
}
}

subprojects {
apply plugin: 'java'

sourceCompatibility = '17'
targetCompatibility = '17'

dependencies {
// 通用依赖,可以在此处添加
implementation 'org.springframework.boot:spring-boot-starter'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
}

// 统一的测试任务配置等(可选)
}

3.2 各子模块配置

3.2.1 domain 模块

功能:定义核心领域模型和业务逻辑。

domain/build.gradle:

1
2
3
dependencies {
// 无需依赖其他模块,专注于领域逻辑
}

示例代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// domain/src/main/java/com/example/domain/User.java
package com.example.domain;

public class User {
private String id;
private String name;
private String email;

// 构造函数、Getters 和 Setters

public User() {}

public User(String id, String name, String email) {
this.id = id;
this.name = name;
this.email = email;
}

// getters and setters
public String getId() {
return id;
}

// ... 其他 getters 和 setters
}

3.2.2 repository 模块

功能:定义仓储接口,供应用层和基础设施层引用。

repository/build.gradle:

1
2
3
dependencies {
implementation project(':domain')
}

示例代码

1
2
3
4
5
6
7
8
9
10
11
// repository/src/main/java/com/example/repository/UserRepository.java
package com.example.repository;

import com.example.domain.User;
import java.util.Optional;
import java.util.List;

public interface UserRepository {
void save(User user);
Optional<User> findById(String id);
}

3.3.3 application 模块

功能:实现应用服务,协调领域模型和仓储接口。

application/build.gradle:

1
2
3
4
5
dependencies {
implementation project(':domain')
implementation project(':repository')
implementation 'org.springframework.boot:spring-boot-starter'
}

示例代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
// application/src/main/java/com/example/application/UserService.java
package com.example.application;

import com.example.domain.User;
import com.example.repository.UserRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.util.List;
import java.util.Optional;

@Service
public class UserService {

private final UserRepository userRepository;

@Autowired
public UserService(UserRepository userRepository){
this.userRepository = userRepository;
}

public void createUser(User user){
userRepository.save(user);
}

public Optional<User> getUserById(String id){
return userRepository.findById(id);
}
}

3.3.4 infrastructure 模块

功能:实现基础设施组件,包括具体的仓储实现。

infrastructure/build.gradle:

1
2
3
4
dependencies {
implementation project(':repository')
implementation 'org.springframework.boot:spring-boot-starter'
}

infrastructure:persistence-mysql子模块

功能:实现 MySQL 的具体仓储。

infrastructure/persistence-mysql/build.gradle:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
plugins {
id 'org.springframework.boot'
id 'io.spring.dependency-management'
}

dependencies {
implementation project(':domain')
implementation project(':repository')
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
runtimeOnly 'mysql:mysql-connector-java'

// 可选:MapStruct 用于对象映射
implementation 'org.mapstruct:mapstruct:1.5.5.Final'
annotationProcessor 'org.mapstruct:mapstruct-processor:1.5.5.Final'
}

示例代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// infrastructure/persistence-mysql/src/main/java/com/example/infrastructure/persistence/mysql/UserEntity.java
package com.example.infrastructure.persistence.mysql;

import javax.persistence.Entity;
import javax.persistence.Id;
import javax.persistence.Table;

@Entity
@Table(name = "users")
public class UserEntity {
@Id
private String id;
private String name;
private String email;

// 构造函数、Getters 和 Setters

public UserEntity() {}

public UserEntity(String id, String name, String email) {
this.id = id;
this.name = name;
this.email = email;
}

// getters and setters
}
1
2
3
4
5
6
7
8
9
// infrastructure/persistence-mysql/src/main/java/com/example/infrastructure/persistence/mysql/ JpaUserRepository.java
package com.example.infrastructure.persistence.mysql;

import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;

@Repository
public interface JpaUserRepository extends JpaRepository<UserEntity, String> {
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
// infrastructure/persistence-mysql/src/main/java/com/example/infrastructure/persistence/mysql/MySQLUserRepository.java
package com.example.infrastructure.persistence.mysql;

import com.example.domain.User;
import com.example.repository.UserRepository;
import org.springframework.stereotype.Repository;
import org.springframework.beans.factory.annotation.Autowired;

import java.util.Optional;
import java.util.List;
import java.util.stream.Collectors;

@Repository
public class MySQLUserRepository implements UserRepository {

private final JpaUserRepository jpaUserRepository;

@Autowired
public MySQLUserRepository(JpaUserRepository jpaUserRepository){
this.jpaUserRepository = jpaUserRepository;
}

@Override
public void save(User user) {
UserEntity entity = new UserEntity(user.getId(), user.getName(), user.getEmail());
jpaUserRepository.save(entity);
}

@Override
public Optional<User> findById(String id) {
Optional<UserEntity> entityOpt = jpaUserRepository.findById(id);
return entityOpt.map(entity -> new User(entity.getId(), entity.getName(), entity.getEmail()));
}
}

infrastructure:persistence-dynamodb 子模块

功能:实现 DynamoDB 的具体仓储。

infrastructure/persistence-dynamodb/build.gradle:

1
2
3
4
5
6
7
8
9
10
11
12
plugins {
id 'org.springframework.boot'
id 'io.spring.dependency-management'
}

dependencies {
implementation project(':domain')
implementation project(':repository')
implementation 'software.amazon.awssdk:dynamodb:2.20.0'
implementation 'com.fasterxml.jackson.core:jackson-databind:2.14.0'
implementation 'org.springframework.boot:spring-boot-starter'
}

示例代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
// infrastructure/persistence-dynamodb/src/main/java/com/example/infrastructure/persistence/dynamodb/DynamoDBUserRepository.java
package com.example.infrastructure.persistence.dynamodb;

import com.example.domain.User;
import com.example.repository.UserRepository;
import org.springframework.stereotype.Repository;
import software.amazon.awssdk.services.dynamodb.DynamoDbClient;
import software.amazon.awssdk.services.dynamodb.model.*;

import org.springframework.beans.factory.annotation.Autowired;
import com.fasterxml.jackson.databind.ObjectMapper;

import java.util.Optional;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;

@Repository
public class DynamoDBUserRepository implements UserRepository {

private static final String TABLE_NAME = "Users";

private final DynamoDbClient dynamoDbClient;
private final ObjectMapper objectMapper;

@Autowired
public DynamoDBUserRepository(DynamoDbClient dynamoDbClient){
this.dynamoDbClient = dynamoDbClient;
this.objectMapper = new ObjectMapper();
}

@Override
public void save(User user) {
try {
String json = objectMapper.writeValueAsString(user);
Map<String, Object> map = objectMapper.readValue(json, Map.class);
Map<String, AttributeValue> item = map.entrySet().stream()
.collect(Collectors.toMap(
Map.Entry::getKey,
e -> AttributeValue.builder().s(e.getValue().toString()).build()
));

PutItemRequest request = PutItemRequest.builder()
.tableName(TABLE_NAME)
.item(item)
.build();

dynamoDbClient.putItem(request);
} catch (Exception e) {
throw new RuntimeException("Failed to save user to DynamoDB", e);
}
}

@Override
public Optional<User> findById(String id) {
GetItemRequest request = GetItemRequest.builder()
.tableName(TABLE_NAME)
.key(Map.of("id", AttributeValue.builder().s(id).build()))
.build();

GetItemResponse response = dynamoDbClient.getItem(request);
if (response.hasItem()) {
try {
String json = objectMapper.writeValueAsString(response.item());
User user = objectMapper.readValue(json, User.class);
return Optional.of(user);
} catch (Exception e) {
throw new RuntimeException("Failed to parse user from DynamoDB", e);
}
}
return Optional.empty();
}
}

DynamoDB 客户端配置

infrastructure:persistence-dynamodb 子模块中配置 DynamoDB 客户端 Bean。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// infrastructure/persistence-dynamodb/src/main/java/com/example/infrastructure/persistence/dynamodb/DynamoDBConfig.java
package com.example.infrastructure.persistence.dynamodb;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import software.amazon.awssdk.services.dynamodb.DynamoDbClient;
import software.amazon.awssdk.regions.Region;

@Configuration
public class DynamoDBConfig {
@Bean
public DynamoDbClient dynamoDbClient() {
return DynamoDbClient.builder()
.region(Region.US_EAST_1) // 根据实际情况选择区域
.build();
}
}

3.3.5 config 模块(可选)

功能:集中管理项目配置,例如选择使用的仓储实现、数据库配置等。

config/build.gradle:

1
2
3
4
5
6
dependencies {
implementation project(':application')
implementation project(':infrastructure:persistence-mysql')
implementation project(':infrastructure:persistence-dynamodb')
implementation 'org.springframework.boot:spring-boot-starter'
}

示例代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
// config/src/main/java/com/example/config/RepositoryConfig.java
package com.example.config;

import com.example.repository.UserRepository;
import com.example.infrastructure.persistence_mysql.MySQLUserRepository;
import com.example.infrastructure.persistence_dynamodb.DynamoDBUserRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class RepositoryConfig {

@Value("${app.repository.type}")
private String repositoryType;

private final MySQLUserRepository mySQLUserRepository;
private final DynamoDBUserRepository dynamoDBUserRepository;

@Autowired
public RepositoryConfig(MySQLUserRepository mySQLUserRepository, DynamoDBUserRepository dynamoDBUserRepository){
this.mySQLUserRepository = mySQLUserRepository;
this.dynamoDBUserRepository = dynamoDBUserRepository;
}

@Bean
public UserRepository userRepository() {
if ("mysql".equalsIgnoreCase(repositoryType)) {
return mySQLUserRepository;
} else if ("dynamodb".equalsIgnoreCase(repositoryType)) {
return dynamoDBUserRepository;
} else {
throw new IllegalArgumentException("Unsupported repository type: " + repositoryType);
}
}
}

**application.properties**(在 api 模块或根模块)

1
2
3
4
# application.properties
app.repository.type=mysql
# 或者
# app.repository.type=dynamodb

3.3.6 api 模块

功能:作为应用的入口,处理外部请求(如 REST API)。

api/build.gradle:

1
2
3
4
5
6
7
8
9
10
plugins {
id 'org.springframework.boot'
id 'io.spring.dependency-management'
}

dependencies {
implementation project(':application')
implementation project(':config')
implementation 'org.springframework.boot:spring-boot-starter-web'
}

示例代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
// api/src/main/java/com/example/api/UserController.java
package com.example.api;

import com.example.application.UserService;
import com.example.domain.User;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;

import java.util.List;
import java.util.Optional;

@RestController
@RequestMapping("/users")
public class UserController {

private final UserService userService;

@Autowired
public UserController(UserService userService){
this.userService = userService;
}

@PostMapping
public void createUser(@RequestBody User user){
userService.createUser(user);
}

@GetMapping("/{id}")
public Optional<User> getUserById(@PathVariable String id){
return userService.getUserById(id);
}
}

3.4 模块间依赖关系图

1
2
3
4
5
6
7
8
9
10
11
domain

repository

application

infrastructure

config

api
  • domain 是最底层,其他模块依赖于它。
  • repository 依赖于 **domain**。
  • application 依赖于 repository 和 **domain**。
  • infrastructure 依赖于 repository 和 **domain**,实现具体仓储。
  • config 依赖于 application 和 **infrastructure**,进行配置管理。
  • api 依赖于 application 和 **config**,作为应用入口。

4. 实施步骤详解

4.1 创建各个模块

使用 Gradle 命令或 IDE 创建各个模块。例如,使用命令行:

1
2
3
4
5
6
7
mkdir ddd-project
cd ddd-project
gradle init --type basic
# 创建子模块目录
mkdir domain repository application infrastructure
mkdir infrastructure/persistence-mysql infrastructure/persistence-dynamodb
mkdir config api

然后,在各个子模块目录下创建相应的 build.gradle 文件并添加内容,如上所示。

4.2 配置依赖关系

确保每个子模块的 build.gradle 文件中正确地声明依赖关系。例如,在 application 模块的 build.gradle 中:

1
2
3
4
5
dependencies {
implementation project(':domain')
implementation project(':repository')
implementation 'org.springframework.boot:spring-boot-starter'
}

类似地,在其他模块中声明所需的依赖。

4.3 配置 SpringBoot

api 模块中,添加 SpringBoot应用的主类:

1
2
3
4
5
6
7
8
9
10
11
12
13
// api/src/main/java/com/example/api/MyDddProjectApplication.java
package com.example.api;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication(scanBasePackages = "com.example")
public class MyDddProjectApplication {

public static void main(String[] args) {
SpringApplication.run(MyDddProjectApplication.class, args);
}
}

说明

  • 使用 @SpringBootApplication 注解并设置 scanBasePackagescom.example,确保 Spring 能扫描到所有子模块中的组件。

4.4 配置应用属性

api 模块中创建 src/main/resources/application.properties,并添加必要的配置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# MySQL 配置
spring.datasource.url=jdbc:mysql://localhost:3306/mydb
spring.datasource.username=root
spring.datasource.password=secret
spring.jpa.hibernate.ddl-auto=update
spring.jpa.show-sql=true

# 仓储选择
app.repository.type=mysql
# 或者
# app.repository.type=dynamodb

# DynamoDB 配置(如适用)
# aws.accessKeyId=YOUR_ACCESS_KEY
# aws.secretAccessKey=YOUR_SECRET_KEY
# aws.region=us-east-1

4.5 构建和运行

在根项目目录下,运行以下命令以构建和运行应用:

1
2
./gradlew build
./gradlew :api:bootRun

确保你的 MySQL 和 DynamoDB(如果选择使用)都已正确配置和运行。

5. 项目优化

5.1 使用 Spring Profiles

为了更灵活地在不同环境(如开发、测试、生产)中选择仓储实现,可以使用 Spring Profiles。

示例:

MySQLUserRepositoryDynamoDBUserRepository 上添加 @Profile 注解。

1
2
3
4
5
6
7
8
9
10
11
12
13
// MySQLUserRepository.java
@Repository
@Profile("mysql")
public class MySQLUserRepository implements UserRepository {
// ...
}

// DynamoDBUserRepository.java
@Repository
@Profile("dynamodb")
public class DynamoDBUserRepository implements UserRepository {
// ...
}

application.properties 中指定活跃配置:

1
2
3
spring.profiles.active=mysql
# 或者
# spring.profiles.active=dynamodb

5.2 使用 MapStruct进行对象映射

手动在仓储实现中转换领域对象和持久化对象可能繁琐,使用 MapStruct 可以简化这一过程。

示例

添加 MapStruct 依赖(已在 persistence-mysql 模块中添加)。

定义映射接口:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// infrastructure/persistence-mysql/src/main/java/com/example/infrastructure/persistence/mysql/UserMapper.java
package com.example.infrastructure.persistence.mysql;

import com.example.domain.User;
import org.mapstruct.Mapper;
import org.mapstruct.factory.Mappers;

@Mapper
public interface UserMapper {
UserMapper INSTANCE = Mappers.getMapper(UserMapper.class);

UserEntity toEntity(User user);
User toDomain(UserEntity entity);
}

MySQLUserRepository 中使用 UserMapper 进行转换:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
// MySQLUserRepository.java
package com.example.infrastructure.persistence.mysql;

import com.example.domain.User;
import com.example.repository.UserRepository;
import org.springframework.stereotype.Repository;
import org.springframework.beans.factory.annotation.Autowired;

import java.util.Optional;
import java.util.List;
import java.util.stream.Collectors;

@Repository
@Profile("mysql")
public class MySQLUserRepository implements UserRepository {

private final JpaUserRepository jpaUserRepository;
private final UserMapper userMapper = UserMapper.INSTANCE;

@Autowired
public MySQLUserRepository(JpaUserRepository jpaUserRepository){
this.jpaUserRepository = jpaUserRepository;
}

@Override
public void save(User user) {
UserEntity entity = userMapper.toEntity(user);
jpaUserRepository.save(entity);
}

@Override
public Optional<User> findById(String id) {
Optional<UserEntity> entityOpt = jpaUserRepository.findById(id);
return entityOpt.map(userMapper::toDomain);
}
}

5.3 增加测试模块

为每个模块编写单元测试和集成测试,确保各部分功能正常。

5.4 使用依赖注入选择仓储实现

config 模块中动态选择仓储实现,或使用工厂模式进一步封装。

6. 总结

本文,我们没有讲解 DDD那些烧脑的理论知识,而是按照 DDD 的分层原则详细地落地了一个user添加和查询功能,并实现支持多种持久化机制(MySQL 和 DynamoDB)的仓储层设计。只要你有过 MVC 的 web开发经验,应该可以快速理解上述 DDD的代码工程。

对于 DDD模块化设计带来的好处,可以总结为以下几点:

  • 高内聚、低耦合:每个模块都有明确的职责,模块之间通过接口进行通信,降低了耦合度。
  • 易于扩展:未来添加新的持久化机制(如 PostgreSQL、Redis 等)时,只需新增相应的基础设施子模块,实现仓储接口即可。
  • 团队协作:不同团队或开发者可以并行开发不同模块,减少冲突和依赖问题。
  • 可维护性:清晰的模块边界和职责分离,使得代码更易于理解和维护。

在实际项目中,我们可根据具体的业务需求灵活地调整模块划分和依赖关系,确保架构设计既符合业务需求,又具备良好的技术基础。

7. 学习交流

如果你觉得文章有帮助,请帮忙转发给更多的好友,或关注公众号:猿java,持续输出硬核文章。

drawing