DDD 很難,推薦一套小白也能輕松落地的方案!
DDD是微服務(wù)中經(jīng)常用到的一種架構(gòu)方式,在實際工作中,我們該如何快速落地一個 DDD工程呢?這篇文章,我們將手把手帶你落地一個 DDD項目,不管你有沒有 DDD經(jīng)驗,都可以輕松使用。
在開始我們的文章之前,我們還是要簡單的了解下 DDD是什么,幫助我們下面更好地理解代碼工程。
一、什么是DDD?
DDD,全稱 Domain-Driven Design,翻譯為領(lǐng)域驅(qū)動設(shè)計,它是一種軟件開發(fā)方法論,由埃里克·埃文斯(Eric Evans) 在其2003年出版的同名書籍中提出。DDD旨在通過密切關(guān)注復(fù)雜軟件系統(tǒng)的核心業(yè)務(wù)領(lǐng)域,將業(yè)務(wù)需求與技術(shù)實現(xiàn)緊密結(jié)合,從而提高軟件的可維護性、可擴展性和靈活性。
1. DDD 的核心理念
DDD 以領(lǐng)域為核心,強調(diào)將業(yè)務(wù)領(lǐng)域作為軟件開發(fā)的核心,致力于深入理解業(yè)務(wù)需求和業(yè)務(wù)規(guī)則,通過建模來反映實際業(yè)務(wù)問題。
2. 統(tǒng)一語言
DDD使得開發(fā)團隊和業(yè)務(wù)專家共同使用的一種準(zhǔn)確、一致的語言(Ubiquitous Language),用于描述業(yè)務(wù)領(lǐng)域中的概念、流程和規(guī)則,減少溝通障礙,提高理解一致性。
3. 戰(zhàn)略設(shè)計與戰(zhàn)術(shù)設(shè)計
DDD 分為戰(zhàn)略設(shè)計和戰(zhàn)術(shù)設(shè)計兩部分:
- 戰(zhàn)略設(shè)計:關(guān)注整個系統(tǒng)的高層次結(jié)構(gòu)和模塊劃分,定義不同的子域(Subdomain)和上下文邊界(Bounded Context)。
- 戰(zhàn)術(shù)設(shè)計:關(guān)注特定上下文內(nèi)的細(xì)節(jié),實現(xiàn)領(lǐng)域模型和相關(guān)組件。
說實話,DDD的理論確實很燒腦,我們會在后續(xù)的文章中慢慢拆解。不管怎樣,在對 DDD有了簡單的了解之后,我們要進(jìn)入今天的核心部分:DDD代碼實操。
本文目標(biāo):使用DDD + SpringBoot + JPA + 雙數(shù)據(jù)源(MySQL + DynamoDB)實現(xiàn)對 user表進(jìn)行添加和查詢功能,完全適合小白操作。
二、項目整體結(jié)構(gòu)
首先,我們先看下整個工程建的主要模塊以及模塊之間的依賴關(guān)系:
- domain:核心領(lǐng)域模型和業(yè)務(wù)邏輯。
- repository:倉儲接口定義。
- application:應(yīng)用服務(wù)層,協(xié)調(diào)領(lǐng)域?qū)ο蠛蛡}儲。
- infrastructure:基礎(chǔ)設(shè)施層,包括具體的倉儲實現(xiàn)(MySQL 和 DynamoDB)、配置等。
- config:獨立的配置模塊(可選,視項目復(fù)雜程度而定)。
- api(或 web):入口層,如 REST API 控制器,主要處理外部接口請求。
模塊結(jié)構(gòu)如下:
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
三、項目模塊詳解
1. Gradle 配置
(1) 根項目 settings.gradle
在根項目的 settings.gradle 中,包含所有子模塊:
rootProject.name = 'ddd-project'
include 'domain'
include 'repository'
include 'application'
include 'infrastructure'
include 'infrastructure:persistence-mysql'
include 'infrastructure:persistence-dynamodb'
include 'config'
include 'api'
(2) 根項目 build.gradle
根項目的 build.gradle 通常用于定義全局的插件和依賴管理。
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'
}
// 統(tǒng)一的測試任務(wù)配置等(可選)
}
2. 各子模塊配置
(1) domain 模塊
功能:定義核心領(lǐng)域模型和業(yè)務(wù)邏輯。
domain/build.gradle:
dependencies {
// 無需依賴其他模塊,專注于領(lǐng)域邏輯
}
示例代碼:
// domain/src/main/java/com/example/domain/User.java
package com.example.domain;
publicclass User {
private String id;
private String name;
private String email;
// 構(gòu)造函數(shù)、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
}
(2) repository 模塊
功能:定義倉儲接口,供應(yīng)用層和基礎(chǔ)設(shè)施層引用。
repository/build.gradle:
dependencies {
implementation project(':domain')
}
示例代碼:
// 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) application 模塊
功能:實現(xiàn)應(yīng)用服務(wù),協(xié)調(diào)領(lǐng)域模型和倉儲接口。
application/build.gradle:
dependencies {
implementation project(':domain')
implementation project(':repository')
implementation 'org.springframework.boot:spring-boot-starter'
}
示例代碼:
// 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
publicclass UserService {
privatefinal 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);
}
}
(4) infrastructure 模塊
功能:實現(xiàn)基礎(chǔ)設(shè)施組件,包括具體的倉儲實現(xiàn)。
infrastructure/build.gradle:
dependencies {
implementation project(':repository')
implementation 'org.springframework.boot:spring-boot-starter'
}
① infrastructure:persistence-mysql子模塊
功能:實現(xiàn) MySQL 的具體倉儲。
infrastructure/persistence-mysql/build.gradle:
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'
}
示例代碼:
// 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")
publicclass UserEntity {
@Id
private String id;
private String name;
private String email;
// 構(gòu)造函數(shù)、Getters 和 Setters
public UserEntity() {}
public UserEntity(String id, String name, String email) {
this.id = id;
this.name = name;
this.email = email;
}
// getters and setters
}
// 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> {
}
// 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
publicclass MySQLUserRepository implements UserRepository {
privatefinal 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 子模塊
功能:實現(xiàn) DynamoDB 的具體倉儲。
infrastructure/persistence-dynamodb/build.gradle:
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'
}
示例代碼:
// 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
publicclass DynamoDBUserRepository implements UserRepository {
privatestaticfinal String TABLE_NAME = "Users";
privatefinal DynamoDbClient dynamoDbClient;
privatefinal 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) {
thrownew 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) {
thrownew RuntimeException("Failed to parse user from DynamoDB", e);
}
}
return Optional.empty();
}
}
③ DynamoDB 客戶端配置
在 infrastructure:persistence-dynamodb 子模塊中配置 DynamoDB 客戶端 Bean。
// 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
publicclass DynamoDBConfig {
@Bean
public DynamoDbClient dynamoDbClient() {
return DynamoDbClient.builder()
.region(Region.US_EAST_1) // 根據(jù)實際情況選擇區(qū)域
.build();
}
}
(5) config 模塊(可選)
功能:集中管理項目配置,例如選擇使用的倉儲實現(xiàn)、數(shù)據(jù)庫配置等。
config/build.gradle:
dependencies {
implementation project(':application')
implementation project(':infrastructure:persistence-mysql')
implementation project(':infrastructure:persistence-dynamodb')
implementation 'org.springframework.boot:spring-boot-starter'
}
示例代碼:
// 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
publicclass RepositoryConfig {
@Value("${app.repository.type}")
private String repositoryType;
privatefinal MySQLUserRepository mySQLUserRepository;
privatefinal 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;
} elseif ("dynamodb".equalsIgnoreCase(repositoryType)) {
return dynamoDBUserRepository;
} else {
thrownew IllegalArgumentException("Unsupported repository type: " + repositoryType);
}
}
}
**application.properties**(在 api 模塊或根模塊)
# application.properties
app.repository.type=mysql
# 或者
# app.repository.type=dynamodb
(6) api 模塊
功能:作為應(yīng)用的入口,處理外部請求(如 REST API)。
api/build.gradle:
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'
}
示例代碼:
// 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")
publicclass UserController {
privatefinal 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);
}
}
4. 模塊間依賴關(guān)系圖
domain
↑
repository
↑
application
↑
infrastructure
↑
config
↑
api
- domain 是最底層,其他模塊依賴于它。
- repository 依賴于 **domain**。
- application 依賴于 repository 和 **domain**。
- infrastructure 依賴于 repository 和 **domain**,實現(xiàn)具體倉儲。
- config 依賴于 application 和 **infrastructure**,進(jìn)行配置管理。
- api 依賴于 application 和 **config**,作為應(yīng)用入口。
四、實施步驟詳解
1. 創(chuàng)建各個模塊
使用 Gradle 命令或 IDE 創(chuàng)建各個模塊。例如,使用命令行:
mkdir ddd-project
cd ddd-project
gradle init --type basic
# 創(chuàng)建子模塊目錄
mkdir domain repository application infrastructure
mkdir infrastructure/persistence-mysql infrastructure/persistence-dynamodb
mkdir config api
然后,在各個子模塊目錄下創(chuàng)建相應(yīng)的 build.gradle 文件并添加內(nèi)容,如上所示。
2. 配置依賴關(guān)系
確保每個子模塊的 build.gradle 文件中正確地聲明依賴關(guān)系。例如,在 application 模塊的 build.gradle 中:
dependencies {
implementation project(':domain')
implementation project(':repository')
implementation 'org.springframework.boot:spring-boot-starter'
}
類似地,在其他模塊中聲明所需的依賴。
3. 配置 SpringBoot
在 api 模塊中,添加 SpringBoot應(yīng)用的主類:
// 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 注解并設(shè)置 scanBasePackages 為 com.example,確保 Spring 能掃描到所有子模塊中的組件。
4. 配置應(yīng)用屬性
在 api 模塊中創(chuàng)建 src/main/resources/application.properties,并添加必要的配置:
# 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
5. 構(gòu)建和運行
在根項目目錄下,運行以下命令以構(gòu)建和運行應(yīng)用:
./gradlew build
./gradlew :api:bootRun
確保你的 MySQL 和 DynamoDB(如果選擇使用)都已正確配置和運行。
五、項目優(yōu)化
1. 使用 Spring Profiles
為了更靈活地在不同環(huán)境(如開發(fā)、測試、生產(chǎn))中選擇倉儲實現(xiàn),可以使用 Spring Profiles。
示例:
在 MySQLUserRepository 和 DynamoDBUserRepository 上添加 @Profile 注解。
// MySQLUserRepository.java
@Repository
@Profile("mysql")
public class MySQLUserRepository implements UserRepository {
// ...
}
// DynamoDBUserRepository.java
@Repository
@Profile("dynamodb")
public class DynamoDBUserRepository implements UserRepository {
// ...
}
在 application.properties 中指定活躍配置:
spring.profiles.active=mysql
# 或者
# spring.profiles.active=dynamodb
2. 使用 MapStruct進(jìn)行對象映射
手動在倉儲實現(xiàn)中轉(zhuǎn)換領(lǐng)域?qū)ο蠛统志没瘜ο罂赡芊爆崳褂?MapStruct 可以簡化這一過程。
示例:
添加 MapStruct 依賴(已在 persistence-mysql 模塊中添加)。
定義映射接口:
// 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 進(jìn)行轉(zhuǎn)換:
// 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")
publicclass MySQLUserRepository implements UserRepository {
privatefinal JpaUserRepository jpaUserRepository;
privatefinal 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);
}
}
3. 增加測試模塊
為每個模塊編寫單元測試和集成測試,確保各部分功能正常。
4. 使用依賴注入選擇倉儲實現(xiàn)
在 config 模塊中動態(tài)選擇倉儲實現(xiàn),或使用工廠模式進(jìn)一步封裝。
六、總結(jié)
本文,我們沒有講解 DDD那些燒腦的理論知識,而是按照 DDD 的分層原則詳細(xì)地落地了一個user添加和查詢功能,并實現(xiàn)支持多種持久化機制(MySQL 和 DynamoDB)的倉儲層設(shè)計。只要你有過 MVC 的 web開發(fā)經(jīng)驗,應(yīng)該可以快速理解上述 DDD的代碼工程。
對于 DDD模塊化設(shè)計帶來的好處,可以總結(jié)為以下幾點:
- 高內(nèi)聚、低耦合:每個模塊都有明確的職責(zé),模塊之間通過接口進(jìn)行通信,降低了耦合度。
- 易于擴展:未來添加新的持久化機制(如 PostgreSQL、Redis 等)時,只需新增相應(yīng)的基礎(chǔ)設(shè)施子模塊,實現(xiàn)倉儲接口即可。
- 團隊協(xié)作:不同團隊或開發(fā)者可以并行開發(fā)不同模塊,減少沖突和依賴問題。
- 可維護性:清晰的模塊邊界和職責(zé)分離,使得代碼更易于理解和維護。
在實際項目中,我們可根據(jù)具體的業(yè)務(wù)需求靈活地調(diào)整模塊劃分和依賴關(guān)系,確保架構(gòu)設(shè)計既符合業(yè)務(wù)需求,又具備良好的技術(shù)基礎(chǔ)。