项目架构概览
在基于 DDD 分层架构与 Spring Boot 构建的项目中,主要包含以下几个关键 module,通过这些模块实现功能的模块化与职责清晰划分。
domain(领域层)
这一层是整个项目的核心,包含领域模型、领域服务、聚合根等核心业务概念。以电商系统为例,订单(Order)聚合根及其相关领域服务(如计算订单总价、订单商品等)就存在于此模块。
application(应用层)
应用层负责协调领域层的服务来完成应用的用例。它本身不包含具体业务逻辑,而是对业务逻辑进行编排。比如,接收来自外部的订单创建请求,调用领域层的订单服务来创建订单。
infrastructure(基础设施层)
该层提供技术相关的支持,涵盖数据库访问、消息队列、文件存储等功能。在订单服务中,访问数据库来存储订单信息就属于这一层的范畴。
interface(接口层)
接口层负责与外部交互,包括接收 HTTP 请求(如 Controller)、消息监听等。它将外部请求转换为应用层可处理的命令或事件。例如,接收用户下单的 HTTP 请求,并将其转换为应用层创建订单的指令。
common(公共层)
common 层在项目中**扮演着提供通用功能和工具的角色。**它包含一些可以被其他各层复用的代码,比如通用的工具类(如日期处理工具、字符串处理工具)、通用的异常处理机制、常量定义等。以电商项目为例,可能会有一个OrderMathUtils类,其中包含计算商品折扣金额的通用方法,这个方法可以在domain层的OrderDiscountService中被调用,也可能在application层的一些业务编排逻辑中使用。
父级 pom 核心标签作用
在 Maven 项目中,父级 pom 起着关键的管理作用,它负责管理整个项目的依赖、插件以及版本信息等。下面介绍几个核心标签的作用。
groupId、artifactId、version
groupId定义项目所属的组织或公司,通常采用反向域名形式,例如com.example。
artifactId是项目的唯一标识符,比如my - ddd - project。
version指定项目的版本号,如1.0.0。这三个标签共同确定了项目在 Maven 仓库中的坐标,当其他模块或项目依赖此项目时,需要使用这些坐标进行引用。
dependencyManagement
此标签用于管理项目的依赖版本,****主要用于声明依赖,它本身并不直接下载依赖。在父级 pom 中声明依赖的版本号,子 module 可以直接引用这些依赖,而无需再次指定版本。例如:
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<version>2.6.3</version>
</dependency>
</dependencies>
</dependencyManagement>
这样在子 module 中引入spring-boot-starter-web依赖时,只需:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
这种方式确保了整个项目依赖版本的一致性,极大地方便了版本升级和管理工作。
modules
此标签用于声明项目包含的子 module。例如:
<modules>
<module>domain</module>
<module>application</module>
<module>infrastructure</module>
<module>interface</module>
<module>common</module>
</modules>
通过这种声明,Maven 能够统一管理子 module 的构建、测试、部署等操作。当在父级 pom 执行mvn install时,Maven 会按照modules中声明的顺序依次构建每个子 module。
properties
properties标签用于定义项目中的一些通用属性,方便在整个项目中复用。例如:
<properties>
<java.version>11</java.version>
<spring.boot.version>2.6.3</spring.boot.version>
</properties>
在定义了这些属性后,在依赖或者插件中就可以通过${属性名}的方式引用。如:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<version>${spring.boot.version}</version>
</dependency>
这样做的好处是,如果需要修改 Spring Boot 的版本,只需要在标签中修改spring.boot.version的值,而不需要在每个依赖中逐个修改版本号,提高了项目配置的可维护性。
父级 pom 与子 module 的关系
父级 pom 就像项目的管理者,子 module 是具体的执行者。父级 pom 管理着子 module 的依赖、插件等公共配置,子 module 继承父级 pom 的配置,并可根据自身需求进行覆盖或扩展。
继承关系
子 module 通过标签继承父级 pom 的配置。例如,domain模块的 pom 文件示例如下:
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema - instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven - 4.0.0.xsd">
<parent>
<groupId>com.example</groupId>
<artifactId>my-ddd-project</artifactId>
<version>1.0.0</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>domain</artifactId>
</project>
通过这种继承,domain模块继承了父级 pom 的依赖管理、插件管理等配置,减少了重复配置,提升了项目的一致性和可维护性。其他模块如application、infrastructure、interface、common的 pom 文件也类似,只是标签的值分别为各自模块的名称。
依赖传递
父级 pom 中声明的依赖会传递到子 module 中。若父级 pom 中声明了spring-boot-starter-web依赖,子 module 无需再次声明就可使用相关的类和功能。不过,子 module 也能根据自身需要添加额外的依赖,或者排除父级 pom 中传递过来的不需要的依赖。例如,interface模块可能需要添加spring-fox依赖来生成 API 文档,其 pom 文件中可以这样添加:
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema - instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven - 4.0.0.xsd">
<parent>
<groupId>com.example</groupId>
<artifactId>my-ddd-project</artifactId>
<version>1.0.0</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>interface</artifactId>
<dependencies>
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox - swagger2</artifactId>
<version>2.9.2</version>
</dependency>
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox - swagger - ui</artifactId>
<version>2.9.2</version>
</dependency>
</dependencies>
</project>
构建关系
当在父级 pom 执行构建命令(如mvn install)时,Maven 会依据标签中声明的顺序依次构建每个子 module。这确保了整个项目按照正确的顺序进行构建,例如先构建domain模块,因为其他模块可能依赖domain模块中的领域模型和服务。
父级 pom 与子 module 的关系
父级 pom 在整个项目中扮演着管理者的角色,它掌控着子 module 的依赖、插件等公共配置,子 module 则继承父级 pom 的配置,并可依据自身业务需求进行个性化调整。各子模块在dependency部分引入其他模块的情况,深刻反映了模块间的协作关系。
domain 模块
domain模块是项目的业务核心,承载着领域模型、领域服务和聚合根等关键元素。在其 pom 文件中,配置如下:
<dependencies>
<dependency>
<groupId>com.example</groupId>
<artifactId>common</artifactId>
</dependency>
</dependencies>
这里引入com.example组织下的my-ddd-project-common模块,借助其中的通用工具类、常量等,像前文提及的Util类,可用于计算折扣金额,为领域模型的构建和业务规则的执行提供便利。
application 模块
application模块主要负责协调业务用例,将外部请求转化为领域层可处理的操作。其 pom 文件为:
<dependencies>
<dependency>
<groupId>com.example</groupId>
<artifactId>domain</artifactId>
<version>1.0.0</version>
</dependency>
<dependency>
<groupId>com.example</groupId>
<artifactId>infrastructure</artifactId>
<version>1.0.0</version>
</dependency>
<dependency>
<groupId>com.example</groupId>
<artifactId>common</artifactId>
<version>1.0.0</version>
</dependency>
</dependencies>
由于业务协调的需要,它依赖domain模块,调用其中的领域服务,如OrderApplicationService调用OrderDiscountService来计算订单折扣。同时依赖infrastructure模块,通过OrderRepository接口与数据库交互,实现订单数据的持久化。引入common模块,利用其通用功能辅助业务编排逻辑,例如使用通用的日期处理工具类来处理订单时间相关业务。
infrastructure 模块
infrastructure模块为整个项目提供技术支撑,涵盖数据库访问、消息队列等功能。其 pom 文件如下:
<dependencies>
<dependency>
<groupId>com.example</groupId>
<artifactId>domain</artifactId>
<version>1.0.0</version>
</dependency>
</dependencies>
这里是否引入要看团队规范,都可以。以实际项目中为例OrderRepository为例,具体将OrderRepository放在哪个模块,需要根据项目的规模、复杂性、团队的技术偏好以及对架构设计原则的理解和权衡来决定。有些项目可能会采用更严格的分层架构,将OrderRepository及其实现放在infrastructure模块,以实现更彻底的分离;而在一些强调领域模型核心地位和遵循 DDD 原则的项目中,则可能会将OrderRepository定义在domain模块,以更好地体现领域驱动的设计思想。
interface 模块
interface模块负责与外部交互,接收 HTTP 请求、消息监听等。其 pom 文件配置如下:
<dependency>
<groupId>com.example</groupId>
<artifactId>application</artifactId>
<version>1.0.0</version>
</dependency>
为实现与外部的交互,它依赖application模块,调用应用层服务,如OrderController调用OrderApplicationService来处理用户下单请求。common模块不用引入,application模块里已经有了,使用其通用的异常处理机制,当外部请求出现异常时,能够进行统一、规范的处理,提升系统的稳定性和用户体验。
common 模块
common模块主要提供通用功能,供其他模块复用。其 pom 文件为:
<dependencies>
<!-- 可能引入一些通用的第三方库,如常用的日志库等 -->
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
</dependency>
</dependencies>
该模块通常不依赖其他模块的业务逻辑,而是依赖一些通用的第三方库,如引入slf4j-api用于日志记录。通过这种方式,为其他各模块提供统一的日志记录功能,在项目的调试与运维过程中,便于集中管理和分析日志信息,提高项目的可维护性。
电商中订单服务的 DDD 实战案例
领域层(domain)
聚合根与实体
订单(Order)作为一个聚合根,包含多个订单行(OrderItem)实体。订单聚合根维护着订单的整体状态和业务规则,如订单的创建、取消、支付等操作。订单行实体则表示订单中的具体商品信息,包括商品名称、数量、价格等。相关代码如下:
领域服务
在订单领域中,有些复杂的业务逻辑无法简单地归属到某个实体中,这时就需要领域服务。例如,计算订单的折扣金额可能涉及多种折扣规则(如满减、会员折扣等),可以将此逻辑封装在一个OrderDiscountService领域服务中。代码实现如下:
@Service
public class OrderDiscountService {
public BigDecimal calculateDiscount(Order order) {
// 复杂的折扣计算逻辑
BigDecimal totalPrice = order.calculateTotalPrice();
if (order.getCustomerId()!= null) {
// 根据会员等级计算折扣
}
// 其他折扣规则
return discountAmount;
}
}
应用层(application)
应用层负责处理用户的用例。以创建订单的用例为例,可以在OrderApplicationService中实现。它接收来自接口层的创建订单请求,调用领域层的服务来创建订单。代码如下:
@Service
public class OrderApplicationService {
private final OrderRepository orderRepository;
private final OrderDiscountService orderDiscountService;
public OrderApplicationService(OrderRepository orderRepository,
OrderDiscountService orderDiscountService) {
this.orderRepository = orderRepository;
this.orderDiscountService = orderDiscountService;
}
public Long createOrder(CreateOrderCommand command) {
List<OrderItem> orderItems = command.getOrderItems();
Order order = new Order();
order.createOrder(command.getCustomerId(), orderLines);
BigDecimal discount = orderDiscountService.calculateDiscount(order);
order.applyDiscount(discount);
orderRepository.save(order);
return order.getId();
}
}
基础设施层(infrastructure)
数据持久化
在基础设施层实现订单数据的持久化。
@Servcie
public interface OrderRepository{}
消息队列(可选)
如果订单服务需要与其他系统进行异步通信,例如在订单创建成功后发送消息通知库存系统更新库存,可以使用消息队列。以 Kafka 为例,配置 Kafka 生产者和消费者:
@Configuration
public class KafkaConfig {
@Bean
public ProducerFactory<String, String> producerFactory() {
Map<String, Object> configProps = new HashMap<>();
configProps.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, "localhost:9092");
configProps.put(ConsumerConfig.KEY_SERIALIZER_CLASS_CONFIG, StringSerializer.class);
configProps.put(ConsumerConfig.VALUE_SERIALIZER_CLASS_CONFIG, StringSerializer.class);
return new DefaultKafkaProducerFactory<>(configProps);
}
@Bean
public KafkaTemplate<String, String> kafkaTemplate() {
return new KafkaTemplate<>(producerFactory());
}
}
接口层(interface)
接口层使用 Spring MVC 接收 HTTP 请求。创建OrderController来处理与订单相关的请求,如创建订单。代码如下:
@RestController
@RequestMapping("/orders")
public class OrderController {
private final OrderApplicationService orderApplicationService;
public OrderController(OrderApplicationService orderApplicationService) {
this.orderApplicationService = orderApplicationService;
}
@PostMapping
public ResponseEntity<Long> createOrder(@RequestBody CreateOrderCommand command) {
Long orderId = orderApplicationService.createOrder(command);
return ResponseEntity.ok(orderId);
}
}
在实际应用中,接口层除了简单地调用应用层服务,还需处理请求参数的校验、响应数据的格式转换以及异常处理等工作。例如,对CreateOrderCommand中的参数进行合法性校验,确保订单创建请求包含必要且正确的信息,像客户 ID 不能为 null,订单行列表不能为空等。如果校验失败,应返回合适的 HTTP 状态码及错误信息,以提升系统的健壮性和用户体验。
在响应数据方面,可能需要将领域模型中的数据结构转换为适合前端展示的格式。比如,订单状态在领域模型中可能是一个枚举类型,但在前端展示时,可能需要转换为更友好的字符串描述。此外,接口层还需考虑安全性问题,如对敏感信息进行脱敏处理,防止数据泄露。