在电商系统中创建多规格商品时,你是否遇到过这样的场景?商品服务用三个字段描述 SKU,促销服务用 JSON 结构存储价格策略,订单服务却需要将商品信息扁平化为字符串。这种业务概念的割裂,正是传统开发模式难以应对复杂业务的缩影。领域驱动设计(DDD)的诞生,为这类问题提供了全新的解题思路。
DDD 核心思想
传统开发模式往往陷入 “数据库驱动” 的陷阱:先设计表结构,再填充业务逻辑,导致代码成为 SQL 的附庸。一个典型的贫血模型案例是,5000 行的 OrderService 类中堆砌着下单、支付、库存扣减等所有逻辑,而 Order 对象仅是包含 getter/setter 的数据载体。
DDD 通过领域模型重构这一关系:将 “订单支付成功” 的业务概念映射为 Order.pay () 方法,将 “库存不足” 的规则封装在 Inventory.check () 中。每个领域对象都像乐高积木,通过组合实现复杂业务逻辑。
核心概念
- 通用语言:团队共同定义的业务术语词典。例如电商场景中,“履约” 代表从订单生成到商品送达的全流程,“逆向履约” 特指退货场景。大家基于这套通用语言交流,能避免因对业务术语理解不同产生的沟通障碍,提高协作效率。
- 限界上下文:业务概念的势力范围。不同业务场景下,对同一概念的定义和操作范围可能不同。以 “商品” 为例,商品服务的 “商品” 包含类目属性、SKU 组合,营销服务的 “商品” 则关注促销价、活动标签。每个限界上下文都有自己独立的领域模型,相互之间通过明确的接口交互,这样可以避免不同业务场景下概念混淆,让系统架构更清晰。
- 领域分层架构
-
- 表现层:负责与用户交互,包括 REST API 提供对外接口,以及页面渲染展示给用户界面。例如电商网站的商品展示页面、用户下单操作界面等,都是表现层的范畴。它主要任务是收集用户请求,将请求传递给应用层,并将应用层返回的结果展示给用户。
-
- 应用层:承担事务管理、事件发布等职责。比如在电商系统中,用户下单操作涉及到订单创建、库存扣减等多个操作,应用层要确保这些操作在一个事务中执行,保证数据一致性;当订单状态发生变化时,应用层负责发布相应的领域事件,通知其他相关服务。
-
- 领域层:业务核心所在,包含实体、值对象、聚合根等。实体是具有唯一标识的对象,像订单(Order)以订单 ID 为标识,它具有生命周期和业务逻辑,比如订单可以有创建、支付、取消等操作;值对象是描述特征的无标识对象,如地址(Address),“北京朝阳区某地址” 作为一个值对象,主要用于描述实体的某个特征,它不可变,一旦创建其属性不能被修改;聚合根是保证业务完整性的边界,例如修改订单项(OrderItem)必须通过订单(Order)聚合根来进行,这样能确保整个订单业务的一致性和完整性。
-
- 基础设施:提供底层支撑,像数据库用于存储数据,消息队列用于服务间异步通信。在电商系统中,订单数据存储在数据库中,而当订单状态变更时,通过消息队列将订单状态变更事件发送给库存、物流等相关服务,实现服务间解耦。
实战:DDD 在电商系统的落地
领域模型构建示例
以订单履约场景为例,详细展示相关实体、值对象和聚合根的代码设计:
// 订单ID,作为订单的唯一标识,使用值对象实现
public class OrderId {
private final String value;
public OrderId(String value) {
this.value = value;
}
public String getValue() {
return value;
}
}
// 订单项,具有独立的业务逻辑,是实体
public class OrderItem {
private String itemId;
private String productId;
private int quantity;
private BigDecimal unitPrice;
public OrderItem(String itemId, String productId, int quantity, BigDecimal unitPrice) {
this.itemId = itemId;
this.productId = productId;
this.quantity = quantity;
this.unitPrice = unitPrice;
}
// 计算订单项总金额的业务逻辑
public BigDecimal calculateAmount() {
return unitPrice.multiply(BigDecimal.valueOf(quantity));
}
// 其他业务方法...
}
// 地址,作为描述订单特征的值对象
public class Address {
private String province;
private String city;
private String district;
private String detail;
public Address(String province, String city, String district, String detail) {
this.province = province;
this.city = city;
this.district = district;
this.detail = detail;
}
// 由于是值对象,通常不提供修改方法,如需修改则创建新对象
// 其他获取属性方法...
}
// 订单状态,使用枚举定义
public enum OrderStatus {
CREATED, PAID, SHIPPED, DELIVERED, CANCELED
}
// 订单聚合根
public class Order {
private OrderId id;
private List<OrderItem> items;
private Address shippingAddress;
private OrderStatus status;
public Order(OrderId id, List<OrderItem> items, Address shippingAddress) {
this.id = id;
this.items = items;
this.shippingAddress = shippingAddress;
this.status = OrderStatus.CREATED;
}
public void pay(Payment payment) {
if (payment.getAmount().compareTo(this.total()) != 0) {
throw new PaymentAmountMismatchException();
}
this.status = OrderStatus.PAID;
this.registerDomainEvent(new OrderPaidEvent(this.id));
}
// 计算订单总金额
public BigDecimal total() {
return items.stream()
.map(OrderItem::calculateAmount)
.reduce(BigDecimal.ZERO, BigDecimal::add);
}
// 其他业务方法,如取消订单等...
private void registerDomainEvent(OrderPaidEvent event) {
// 这里可以实现将事件发布到事件总线的逻辑
}
}
项目结构设计示例
以电商订单模块为例,展示基于 DDD 的项目结构:
order-module
├── api
│ ├── dto
│ │ ├── OrderCreateRequestDTO.java
│ │ ├── OrderResponseDTO.java
│ │ └── ...
│ └── controller
│ ├── OrderController.java
│ └── ...
├── application
│ ├── service
│ │ ├── OrderApplicationService.java
│ │ └── ...
│ └── event
│ ├── OrderPaidEvent.java
│ └── ...
├── domain
│ ├── model
│ │ ├── Order.java
│ │ ├── OrderItem.java
│ │ ├── OrderId.java
│ │ ├── Address.java
│ │ └── OrderStatus.java
│ ├── repository
│ │ ├── OrderRepository.java
│ │ └── ...
│ └── service
│ ├── OrderDomainService.java
│ └── ...
├── infrastructure
│ ├── repository
│ │ ├── impl
│ │ │ ├── OrderRepositoryImpl.java
│ │ │ └── ...
│ │ └── OrderRepository.java
│ ├── event
│ │ ├── EventBus.java
│ │ └── EventDispatcher.java
│ └── config
│ ├── DatabaseConfig.java
│ └── MessageQueueConfig.java
└── test
├── api
│ ├── OrderControllerTest.java
│ └── ...
├── application
│ ├── OrderApplicationServiceTest.java
│ └── ...
├── domain
│ ├── OrderDomainServiceTest.java
│ └── ...
└── infrastructure
├── OrderRepositoryImplTest.java
└── ...
在上述项目结构中:
- api 包:负责对外提供接口,dto 子包存放数据传输对象,用于在表现层和应用层之间传递数据;controller 子包存放控制器类,接收外部请求并调用应用层服务。
- application 包:应用层,service 子包中的应用服务类协调领域服务完成业务操作;event 子包定义领域事件。
- domain 包:领域层核心,model 子包存放领域模型相关的实体、值对象、聚合根等;repository 子包定义领域模型的数据访问接口;service 子包存放领域服务,处理跨聚合的业务逻辑。
- infrastructure 包:基础设施层,repository 子包的 impl 子包实现领域层定义的数据访问接口;event 子包实现事件总线和事件分发功能;config 子包存放数据库、消息队列等配置类。
- test 包:存放各层的单元测试和集成测试代码,保证系统的正确性和稳定性。
限界上下文协作
通过领域事件实现服务间解耦:
订单服务发布 OrderPaidEvent 事件,库存服务监听事件并执行库存扣减,物流服务生成运单号。用流程图表示如下:
当订单服务完成订单支付操作,发布 OrderPaidEvent 事件到消息队列(MQ)。库存服务和物流服务从消息队列获取该事件,分别执行库存扣减和生成运单操作。这样各个服务之间不需要直接调用,降低了服务间耦合度,当某个服务业务逻辑发生变化时,对其他服务影响较小,提高了系统的可维护性和扩展性。
设计要素
- 实体:具有唯一标识的对象,如 Order(订单 ID 为标识)。实体在业务流程中具有独立的生命周期和业务行为,例如订单实体可以经历创建、支付、发货、完成等不同状态变化,每个状态变化都伴随着相应业务逻辑处理。
- 值对象:描述特征的无标识对象,如 Address(北京朝阳区某地址)。值对象主要用于描述实体的某些属性,它没有自己独立的唯一标识,通过属性值来区分不同实例,并且值对象通常是不可变的,这样可以保证数据的一致性和稳定性。例如在订单中,收货地址作为值对象,在订单整个生命周期中,如果地址信息不发生变化,就不需要额外的标识来区分不同地址实例。
- 聚合根:保证业务完整性的边界,修改 OrderItem 必须通过 Order 聚合根。聚合根是一个聚合的核心对象,它对外提供统一的操作接口,内部维护聚合内各对象的一致性。比如在订单聚合中,订单项(OrderItem)不能被外部直接修改,必须通过订单(Order)聚合根来进行操作,这样可以确保订单相关业务逻辑的完整性,避免因直接操作订单项导致订单数据不一致等问题。
- 领域服务:跨聚合的逻辑,如跨订单的优惠分摊计算。当电商系统有跨多个订单的优惠活动时,需要计算每个订单应分摊的优惠金额,这就涉及到多个订单聚合之间的逻辑处理,这种逻辑就可以封装在领域服务中。领域服务不隶属于任何一个特定聚合,它协调多个聚合完成复杂业务操作,使业务逻辑更加清晰和可维护。
三、DDD 与 MVC 的范式对比
传统三层架构往往演变为 “Service+DTO+DAO” 模式,导致业务逻辑碎片化。DDD 的革新之处在于:
| 维度 | MVC 架构 | DDD 架构 |
|---|---|---|
| 核心关注点 | 请求响应流程 | 业务概念完整性 |
| 业务逻辑存放位置 | Service 层 | 领域对象内部 |
| 数据传递方式 | 通过 DTO 在各层传递 | 通过领域事件进行状态变更通知 |
| 可维护性 | 修改可能影响多个 Service | 变更局限在限界上下文内 |
一个典型转变是:订单取消逻辑从 Service 层的 cancelOrder 方法,重构为 Order.cancel () 方法,包含库存释放、状态变更、通知生成等原子操作。在 MVC 架构中,订单取消逻辑可能分散在多个 Service 类中,各个 Service 类之间可能存在复杂的调用关系,当业务逻辑发生变化时,修改一处可能影响多个 Service 类;而在 DDD 架构中,订单取消逻辑封装在 Order 实体内部,相关操作都围绕 Order 对象进行,并且库存释放、状态变更等操作都在 Order.cancel () 方法中完成,这种设计使得业务逻辑更加集中,并且由于限界上下文的存在,当订单业务逻辑发生变化时,变更影响范围局限在订单限界上下文内,对其他业务模块影响较小,提高了系统可维护性。
DDD 的适用场景
理想应用场景
- 保险理赔系统:涉及报案、定损、核赔、支付等多环节复杂流程。每个环节都有自己独特的业务规则和领域模型,例如报案环节涉及报案信息登记、报案人信息验证等;定损环节需要对损失进行评估、确定赔偿范围等。通过 DDD 可以将这些不同环节划分成不同限界上下文,构建各自领域模型,更好地处理复杂业务逻辑。
- 医疗信息系统:需要严格区分问诊、检验、药房等子域。不同子域业务差异大,如问诊主要记录患者症状、医生诊断等信息;检验涉及各种检验项目、检验结果生成等。采用 DDD 能够针对每个子域进行独立建模和开发,使系统架构更清晰,满足医疗行业复杂业务需求。
- 供应链金融平台:处理订单、物流、资金的多维度协同。订单涉及订单创建、跟踪;物流包括货物运输、仓储管理;资金涉及支付、结算等。各维度业务相互关联又有各自特点,DDD 通过限界上下文将这些业务解耦,实现多维度协同,保证业务完整性和一致性。
实施成本
对于日均订单量不足 1000 的初创电商,采用完整 DDD 可能过度设计。但当系统出现以下信号时,就是引入 DDD 的最佳时机:
- 新需求开发需要修改 3 个以上 Service 类:说明现有系统业务逻辑耦合度高,不同功能模块之间相互影响大,新需求开发困难。引入 DDD 可以通过限界上下文将业务解耦,降低模块间依赖,提高开发效率。
- 业务方频繁抱怨 “系统不够灵活” :表明系统难以快速响应业务变化。DDD 通过领域模型将业务逻辑与技术实现分离,当业务需求变更时,只需在领域模型层面进行调整,能更好地满足业务灵活性要求。
- 代码库中出现多个重复的业务校验逻辑:体现现有系统代码缺乏合理的抽象和封装。DDD 通过实体、值对象、领域服务等概念将业务逻辑进行封装和复用,消除重复代码,提高代码质量。
在 Spring Cloud Alibaba 微服务体系中,DDD 的实施路径可能是:
- 通过事件风暴工作坊划分限界上下文:团队成员通过头脑风暴,梳理业务流程中发生的事件,以及事件的触发者、参与者等信息,从而识别出不同业务领域,划分限界上下文。例如在电商系统中,通过事件风暴可以确定订单、商品、库存等不同限界上下文。
- 定义各微服务的分层架构:参考前文提到的领域分层架构,每个微服务内部按照表现层、应用层、领域层、基础设施层进行分层设计。例如订单微服务,表现层负责接收和处理用户关于订单的请求,应用层协调订单业务流程,领域层实现订单相关业务逻辑,基础设施层负责订单数据存储等。
- 使用 Nacos 实现配置 / 服务治理:Nacos 可以用于管理微服务的配置信息,以及服务的注册与发现。在 DDD 架构中,不同限界上下文对应的微服务可以通过 Nacos 进行配置管理,方便进行服务治理,例如动态调整服务参数、查看服务状态等。
- 通过 Seata 保证跨服务的分布式事务:当业务操作涉及多个微服务,如订单创建涉及订单服务、库存服务等,需要保证分布式事务一致性。Seata 提供分布式事务解决方案,通过事务协调器、事务管理器等组件,确保跨服务操作要么全部成功,要么全部失败,维护数据一致性。
- 用 Sentinel 实现领域服务的流量控制:Sentinel 可以对领域服务进行流量控制,防止因高并发请求导致服务雪崩。例如在电商促销活动期间,订单服务可能会收到大量请求,通过 Sentinel 可以设置限流规则,对进入订单服务的请求进行流量
欢迎关注 ❤
我们搞了一个免费的面试真题共享群,互通有无,一起刷题进步。
没准能让你能刷到自己意向公司的最新面试题呢。
感兴趣的朋友们可以加我微信:wangzhongyang1993,备注:面试群。