在复杂业务系统开发中,传统架构常面临业务逻辑分散、边界模糊、可扩展性差等问题。领域驱动设计(Domain-Driven Design,DDD)作为一种聚焦业务本质的设计方法论,通过"战略设计定边界、战术设计建模型"的核心思路,为解决复杂业务问题提供了系统化方案。本文将从基础概念、与MVC的差异、核心名词实例、实战方法四个维度,全面拆解DDD的核心价值与落地路径。
一、什么是DDD?
DDD是一种应对复杂业务系统的软件设计方法论,其核心目标是让软件设计与业务需求精准对齐。它并非一套固定的架构规范,而是一套"业务驱动"的思维框架,通过以下核心逻辑解决复杂问题:
- 将业务领域分解为可管理的子域,明确核心业务价值;
- 通过"统一语言"消除业务与技术团队的沟通壁垒;
- 在明确的"界限上下文"内构建领域模型,保障模型的一致性;
- 通过战术设计(聚合、实体、值对象等)将业务规则转化为代码逻辑;
- 通过上下文映射定义跨域协作规则,实现系统的柔性集成。
DDD的核心价值在于"让技术架构服务于业务架构",而非传统开发中"让业务适配技术实现"的逆向逻辑,尤其适用于电商、金融、智慧园区等业务逻辑复杂、需求频繁变化的系统。
二、为什么要用DDD?------与MVC模式的核心差异
MVC(Model-View-Controller)是一种以技术职责为核心的分层架构,适用于业务逻辑简单的CRUD系统。但在复杂业务场景下,MVC的局限性逐渐凸显,而DDD通过聚焦业务边界和模型完整性,弥补了这些不足。二者的核心差异可从以下维度对比:
2.1 核心设计理念差异
MVC以"技术实现"为核心,将系统按"接收请求(Controller)- 业务处理(Service)- 数据存储(Model)"的技术职责分层,开发流程通常是"先设计数据库表,再基于表生成Model和Service",本质是"数据驱动";
DDD以"业务领域"为核心,先梳理业务边界和规则,构建能表达业务本质的领域模型,再基于模型设计技术架构,开发流程是"业务建模 → 边界划分 → 代码实现",本质是"业务驱动"。
2.2 核心问题解决能力差异
|----------|---------------------------------|---------------------------------|
| 对比维度 | MVC模式 | DDD模式 |
| 业务逻辑分布 | 分散在Service层,易形成"上帝服务",代码复用性差 | 集中在领域层(聚合根、实体等),边界清晰,内聚性强 |
| 模型特性 | 贫血模型:Model仅含getter/setter,无业务行为 | 充血模型:领域模型封装业务属性与行为,体现业务规则 |
| 系统边界 | 按功能模块划分,边界模糊,跨模块依赖复杂 | 通过界限上下文明确边界,上下文内模型一致,跨上下文协作规则清晰 |
| 可扩展性 | 业务变更易影响多个Service,修改成本高 | 领域模型隔离变化影响域,新增业务仅需扩展模型,适配性强 |
| 团队协作 | 按技术角色分工(前端/后端/数据库),业务与技术沟通存在壁垒 | 按业务域分工,通过统一语言实现业务与技术团队的协同建模 |
| 适用场景 | 简单CRUD系统(如报表系统、管理后台),业务规则简单 | 复杂业务系统(如电商、金融、智慧园区),业务规则复杂且频繁变化 |
2.3 典型案例:MVC与DDD的代码差异
以智慧园区"空间租赁申请"功能为例,对比两种模式的实现逻辑:
MVC模式(贫血模型+上帝服务)
// 贫血模型:仅含数据,无业务行为
@Data
public class ParkSpace {
private Long id;
private String spaceCode;
private String status; // 空闲/已预定/已租
}
@Data
public class LeasingOrder {
private Long id;
private Long spaceId;
private String customerName;
private String orderStatus;
}
// 上帝服务:集中所有业务逻辑,职责过重
@Service
public class SpaceLeasingService {
@Autowired
private ParkSpaceMapper parkSpaceMapper;
@Autowired
private LeasingOrderMapper leasingOrderMapper;
public LeasingOrder applyForLeasing(LeasingApplyRequest request) {
// 1. 参数校验
if (request.getCustomerName() == null) {
throw new IllegalArgumentException("客户姓名不能为空");
}
// 2. 空间状态校验
ParkSpace space = parkSpaceMapper.selectById(request.getSpaceId());
if (!"空闲".equals(space.getStatus())) {
throw new IllegalStateException("该空间不可租赁");
}
// 3. 创建订单
LeasingOrder order = new LeasingOrder();
order.setSpaceId(request.getSpaceId());
order.setCustomerName(request.getCustomerName());
order.setOrderStatus("待签约");
leasingOrderMapper.insert(order);
// 4. 更新空间状态
space.setStatus("已预定");
parkSpaceMapper.updateById(space);
return order;
}
}
DDD模式(充血模型+边界清晰)
// 值对象:封装客户信息,自带校验规则
public class CustomerInfo implements ValueObject {
private final String name;
private final String phone;
// 构造器中实现业务校验,保障数据完整性
public CustomerInfo(String name, String phone) {
if (name == null || name.trim().isEmpty()) {
throw new IllegalArgumentException("客户姓名不能为空");
}
this.name = name.trim();
this.phone = phone.trim();
}
}
// 聚合根:封装空间租赁的核心业务逻辑
public class Space implements AggregateRoot {
private SpaceId id;
private SpaceCode code;
private SpaceStatus status; // 枚举:空闲/已预定/已租
// 业务行为:租赁申请,封装完整业务规则
public LeasingOrder applyForLeasing(CustomerInfo customer, LeaseTerm term, Rental rental) {
// 状态校验:业务规则内聚于领域模型
if (!this.status.isAvailable()) {
throw new IllegalStateException("该空间不可租赁");
}
// 创建订单:通过订单聚合根的工厂方法保证订单完整性
LeasingOrder order = LeasingOrder.create(this.id, customer, term, rental);
// 更新状态:状态变更内聚,避免外部随意修改
this.status = SpaceStatus.RESERVED;
// 发布领域事件:触发后续流程(如生成合同)
this.registerDomainEvent(new SpaceReservedEvent(this.id, order.getId()));
return order;
}
}
// 应用服务:仅协调流程,不包含核心业务逻辑
@Service
@Transactional
public class SpaceLeasingApplicationService {
@Autowired
private SpaceRepository spaceRepository;
@Autowired
private LeasingOrderRepository orderRepository;
public void applyForLeasing(ApplyForLeasingCommand command) {
// 1. 获取领域对象
Space space = spaceRepository.findById(command.getSpaceId())
.orElseThrow(() -> new SpaceNotFoundException("空间不存在"));
// 2. 转换命令为领域对象
CustomerInfo customer = new CustomerInfo(command.getCustomerName(), command.getCustomerPhone());
LeaseTerm term = new LeaseTerm(command.getStartDate(), command.getEndDate());
Rental rental = new Rental(command.getTotalRent());
// 3. 调用领域对象的业务方法
LeasingOrder order = space.applyForLeasing(customer, term, rental);
// 4. 持久化数据
orderRepository.save(order);
spaceRepository.save(space);
}
}
通过对比可见:DDD将核心业务规则封装于领域模型,实现了"业务逻辑内聚";应用服务仅负责流程协调,职责清晰;通过值对象、聚合根等设计保障了数据一致性,大幅提升了代码的可维护性和可扩展性。
三、DDD核心名词概念(附Java实例)
DDD的核心概念分为"战略设计概念"(用于划分边界)和"战术设计概念"(用于构建模型)两类,以下结合业务场景和Java代码实例逐一解析:
3.1 战略设计核心概念
1. 领域(Domain)与子域(Subdomain)
领域是一个组织所做的事情的总和,即业务的"问题空间"。例如:电商平台的领域包含商品销售、订单管理、支付处理等所有业务活动;智慧园区的领域包含园区资产管理、招商租赁、合同管理等。
子域是将复杂领域分解后的更小、更可管理的部分。根据业务重要性,子域分为三类:
- 核心子域:业务的核心竞争力,决定企业差异化优势(如电商的商品推荐算法、智慧园区的空间资产管理);
- 支撑子域:支持核心业务运行,但不构成竞争优势(如电商的订单管理、智慧园区的合同管理);
- 通用子域:各企业通用,可复用或外包的标准化功能(如用户认证、支付网关、短信服务)。
价值:子域划分决定资源分配------核心子域投入最优资源自主开发,通用子域可直接使用现成方案,避免资源浪费。
2. 界限上下文(Bounded Context)
界限上下文是一个明确定义的范围,在范围内,领域模型的术语、概念和规则具有一致含义,超出范围则可能存在歧义。例如"客户"一词:
- 销售上下文:包含联系人、购买意向、报价历史;
- 账单上下文:仅包含账单地址、付款方式;
- 支持上下文:包含服务等级、历史问题记录。
Java实例:通过包结构划分上下文边界(智慧园区系统)
// 空间管理上下文(包边界即上下文边界)
com.smartpark.spacecontext.domain.model
- Space(空间聚合根)
- SpaceStatus(空间状态枚举)
- SpaceRepository(仓储接口)
// 租赁交易上下文
com.smartpark.leasingcontext.domain.model
- LeasingOrder(租赁订单聚合根)
- CustomerInfo(客户信息值对象)
- LeasingOrderRepository(仓储接口)
// 合同管理上下文
com.smartpark.contractcontext.domain.model
- Contract(合同聚合根)
- ContractStatus(合同状态枚举)
价值:明确的上下文边界保障了模型的一致性,避免"一词多义"导致的代码混乱;同时支持团队按上下文自治开发,提升协作效率。
3. 上下文映射(Context Mapping)
上下文映射定义了不同界限上下文之间的协作规则,解决跨域交互时的模型一致性问题。常见的映射模式有6种,以下结合实例详细解析:
(1)共享内核(Shared Kernel)
核心逻辑:两个上下文共享部分领域模型(如通用的基础概念),共享部分需由两个团队共同维护,确保一致性。
适用场景:两个上下文存在强关联的基础概念,且变更频率低。例如:电商的"商品ID"在商品上下文和订单上下文通用,可作为共享内核。
Java实例:
// 共享内核模块(独立于两个上下文)
com.smartpark.sharedkernel.model
- ProductId(商品ID值对象,共享)
- Money(金额值对象,共享)
// 商品上下文使用共享模型
com.smartpark.productcontext.domain.model
- Product {
private ProductId id;
private Money price;
}
// 订单上下文使用共享模型
com.smartpark.ordercontext.domain.model
- OrderItem {
private ProductId productId;
private Money unitPrice;
}
注意:共享内核需严格控制范围,避免过度共享导致上下文耦合过重。
(2)客户-供应商(Customer-Supplier)
核心逻辑:存在"上游-下游"关系,上游上下文(供应商)提供下游上下文(客户)所需的数据/服务,下游依赖上游的模型定义;上游变更需优先考虑下游的兼容性。
适用场景:下游上下文的业务逻辑依赖上游数据,且交互频繁。例如:租赁交易上下文(下游)依赖空间管理上下文(上游)的"空间状态"数据。
Java实例:
// 上游:空间管理上下文(供应商)提供查询服务
@Service
public class SpaceQueryService {
@Autowired
private SpaceRepository spaceRepository;
// 提供上游模型的查询接口(返回上游模型或DTO)
public SpaceDTO getSpaceById(SpaceId spaceId) {
Space space = spaceRepository.findById(spaceId);
return SpaceDTO.from(space); // 转换为DTO供下游使用
}
}
// 下游:租赁交易上下文(客户)依赖上游服务
@Service
public class LeasingDomainService {
@Autowired
private SpaceQueryService spaceQueryService;
public void validateSpaceAvailability(SpaceId spaceId) {
SpaceDTO space = spaceQueryService.getSpaceById(spaceId);
if (!"AVAILABLE".equals(space.getStatus())) {
throw new IllegalStateException("空间不可租赁");
}
}
}
(3)防腐层(Anticorruption Layer,ACL)
核心逻辑:在外部系统/旧系统与当前上下文之间增加"适配器层",将外部模型转换为当前上下文的模型,避免外部模型污染当前领域模型。
适用场景:集成外部系统(如第三方支付、旧版CRM)或模型设计不规范的系统。例如:智慧园区系统集成第三方支付系统,支付系统的模型与当前上下文模型不一致,需通过防腐层转换。
Java实例:
// 外部支付系统模型(不规范,字段命名混乱)
public class ExternalPayment {
private String payId;
private String userNo;
private BigDecimal payAmt;
private String paySts; // 支付状态
// getter/setter
}
// 防腐层:适配器接口
public interface PaymentAdapter {
PaymentResult processPayment(PaymentCommand command);
}
// 防腐层:适配器实现(转换外部模型与领域模型)
@Service
public class ThirdPartyPaymentAdapter implements PaymentAdapter {
@Autowired
private ExternalPaymentClient externalPaymentClient; // 外部支付客户端
@Override
public PaymentResult processPayment(PaymentCommand command) {
// 1. 领域模型转换为外部模型
ExternalPayment externalPayment = new ExternalPayment();
externalPayment.setPayId(command.getPaymentId().getId());
externalPayment.setUserNo(command.getCustomerId().getId());
externalPayment.setPayAmt(command.getAmount().getAmount());
// 2. 调用外部服务
ExternalPaymentResponse response = externalPaymentClient.pay(externalPayment);
// 3. 外部模型转换为领域模型(避免外部模型侵入领域层)
return new PaymentResult(
PaymentId.of(response.getPayId()),
PaymentStatus.fromExternalStatus(response.getPaySts()), // 状态映射
response.getPayTime()
);
}
}
// 领域层使用防腐层接口(不依赖外部模型)
@Service
public class PaymentDomainService {
private final PaymentAdapter paymentAdapter;
public PaymentDomainService(PaymentAdapter paymentAdapter) {
this.paymentAdapter = paymentAdapter; // 依赖注入适配器
}
public void completePayment(PaymentCommand command) {
PaymentResult result = paymentAdapter.processPayment(command);
if (result.getStatus() == PaymentStatus.SUCCESS) {
// 处理支付成功逻辑
}
}
}
(4)开放主机服务(Open Host Service,OHS)
核心逻辑:一个上下文提供一组标准化的公共API(如REST、RPC),供其他上下文调用,API的模型即该上下文的"发布语言",确保跨域交互的规范性。
适用场景:一个上下文需向多个其他上下文提供服务(如用户中心向订单、商品、支付上下文提供用户信息服务)。
Java实例:用户中心上下文提供开放API
// 开放API接口(标准化)
@RestController
@RequestMapping("/api/v1/users")
public class UserOpenApiController {
@Autowired
private UserQueryService userQueryService;
// 标准化API:查询用户信息
@GetMapping("/{userId}")
public ResponseEntity<UserDTO> getUserById(@PathVariable String userId) {
UserDTO user = userQueryService.getUserById(UserId.of(userId));
return ResponseEntity.ok(user);
}
}
// 其他上下文调用开放API(通过标准化模型交互)
@Service
public class OrderDomainService {
@Autowired
private RestTemplate restTemplate;
public UserDTO getUserInfo(UserId userId) {
// 调用开放API,使用标准化DTO
return restTemplate.getForObject(
"http://user-service/api/v1/users/" + userId.getId(),
UserDTO.class
);
}
}
(5)发布语言(Published Language)
核心逻辑:定义一套标准化的"语言"(如DTO模型、JSON Schema),作为不同上下文之间的交互规范,确保跨域通信的一致性。
适用场景:多个上下文之间存在频繁的跨域数据交互,需要统一数据格式。例如:电商系统的"订单创建"事件在订单、库存、支付上下文之间传递,需定义标准化的事件模型作为发布语言。
Java实例:
// 发布语言:标准化的订单创建事件模型(独立模块,供所有上下文依赖)
com.smartpark.publishedlanguage.event
- OrderCreatedEvent {
private OrderId orderId;
private UserId userId;
private List<OrderItemDTO> items;
private LocalDateTime createdAt;
// 构造器、getter
}
// 订单上下文发布事件(使用发布语言)
@Service
public class OrderDomainService {
@Autowired
private ApplicationEventPublisher eventPublisher;
public void createOrder(OrderCommand command) {
// 创建订单逻辑...
// 发布事件(标准化模型)
eventPublisher.publishEvent(new OrderCreatedEvent(order.getId(), command.getUserId(), items, LocalDateTime.now()));
}
}
// 库存上下文监听事件(使用发布语言)
@Component
public class InventoryEventHandler {
@EventListener
public void handleOrderCreatedEvent(OrderCreatedEvent event) {
// 扣减库存逻辑(基于标准化事件模型)
for (OrderItemDTO item : event.getItems()) {
inventoryService.reduceStock(item.getProductId(), item.getQuantity());
}
}
}
(6)遵奉者(Conformist)
核心逻辑:下游上下文完全遵从上-游上下文的模型和API设计,不做任何模型转换。适用于下游上下文依赖上游,且上游模型稳定、设计规范的场景(如内部系统的上下游依赖)。
注意:下游上下文完全依赖上游,上游变更会直接影响下游,需谨慎使用。
3.2 战术设计核心概念
1. 聚合(Aggregate)与聚合根(Aggregate Root)
聚合是一组相关领域对象的集合,作为数据修改的最小单元(一个事务只修改一个聚合);聚合根是聚合的"入口",外部对聚合内对象的访问必须通过聚合根,由聚合根保证聚合内的业务规则一致性。
例如:订单聚合包含"订单(聚合根)、订单项、配送信息",外部不能直接修改订单项,必须通过订单对象操作。
聚合设计原则:
- 一个聚合对应一个事务边界;
- 聚合应尽量小巧(通常不超过5个对象);
- 聚合之间通过ID关联,而非直接引用对象;
- 聚合根负责聚合内的业务规则校验和状态一致性。
Java实例:订单聚合
// 聚合根:订单(外部唯一入口)
@Entity
public class Order implements AggregateRoot {
@Id
private OrderId id; // 聚合根ID
private UserId userId; // 关联其他聚合(通过ID)
@OneToMany(cascade = CascadeType.ALL, orphanRemoval = true)
private List<OrderItem> items = new ArrayList<>(); // 聚合内对象
private DeliveryInfo deliveryInfo; // 聚合内对象(值对象)
private OrderStatus status;
// 业务方法:添加订单项(外部必须通过聚合根操作)
public void addItem(ProductId productId, int quantity, Money unitPrice) {
// 业务规则校验:只有新建订单可添加项
if (this.status != OrderStatus.CREATED) {
throw new IllegalStateException("非新建订单不可添加订单项");
}
// 聚合内对象创建(由聚合根控制)
this.items.add(new OrderItem(this.id, productId, quantity, unitPrice));
}
// 业务方法:支付订单(保证聚合内状态一致)
public void pay(Money paidAmount) {
if (this.status != OrderStatus.CREATED) {
throw new IllegalStateException("仅新建订单可支付");
}
// 校验支付金额与订单金额一致
if (!this.calculateTotalAmount().equals(paidAmount)) {
throw new IllegalArgumentException("支付金额不匹配");
}
this.status = OrderStatus.PAID;
// 发布领域事件
this.registerDomainEvent(new OrderPaidEvent(this.id));
}
// 聚合内计算逻辑(内聚于聚合根)
private Money calculateTotalAmount() {
return this.items.stream()
.map(OrderItem::calculateSubtotal)
.reduce(Money.ZERO, Money::add);
}
}
// 聚合内对象:订单项(无独立ID,依赖聚合根)
@Entity
public class OrderItem {
@Id
@GeneratedValue
private Long id; // 仅用于持久化,非业务ID
private OrderId orderId; // 关联聚合根
private ProductId productId;
private int quantity;
private Money unitPrice;
// 订单项自身的计算逻辑
public Money calculateSubtotal() {
return this.unitPrice.multiply(this.quantity);
}
}
2. 实体(Entity)
实体是具有唯一标识符(ID)的领域对象,其身份不依赖于属性,即使属性变化,身份依然不变。例如:用户修改姓名和地址后,仍是同一个用户;订单修改配送地址后,仍是同一个订单。
判断标准:是否需要跟踪对象的生命周期?是否需要区分"属性相同但身份不同"的对象?是则为实体。
Java实例:
// 实体:用户(通过用户ID唯一标识)
@Entity
public class User implements Entity {
@Id
private UserId id; // 唯一业务ID(区别于数据库自增ID)
private String name;
private Address address; // 值对象(属性依赖)
// 身份判断:基于ID而非属性
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
User user = (User) o;
return Objects.equals(id, user.id);
}
@Override
public int hashCode() {
return Objects.hash(id);
}
// 业务行为:更新地址(属性变化,身份不变)
public void updateAddress(Address newAddress) {
this.address = newAddress;
}
}
3. 值对象(Value Object)
值对象是通过属性值定义的领域对象,无唯一ID,属性完全相同即视为同一个对象。值对象通常是不可变的(创建后不修改,修改时创建新对象),用于描述实体的状态或特征。
常见示例:地址(街道、城市、邮编)、金额(金额+币种)、日期范围(开始时间+结束时间)。
Java实例:金额值对象(不可变)
public class Money implements ValueObject {
// 不可变属性(final)
private final BigDecimal amount;
private final Currency currency;
// 构造器私有,通过静态方法创建(保证不可变性)
private Money(BigDecimal amount, Currency currency) {
// 业务规则校验:金额非负
if (amount.compareTo(BigDecimal.ZERO) < 0) {
throw new IllegalArgumentException("金额不能为负数");
}
this.amount = amount;
this.currency = currency;
}
// 静态工厂方法:创建值对象
public static Money of(BigDecimal amount, Currency currency) {
return new Money(amount, currency);
}
// 业务行为:金额相加(返回新对象,原对象不变)
public Money add(Money other) {
// 校验币种一致
if (!this.currency.equals(other.currency)) {
throw new IllegalArgumentException("币种不一致,无法相加");
}
return new Money(this.amount.add(other.amount), this.currency);
}
// 相等性判断:基于所有属性
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Money money = (Money) o;
return Objects.equals(amount, money.amount) && Objects.equals(currency, money.currency);
}
@Override
public int hashCode() {
return Objects.hash(amount, currency);
}
// 无setter方法,保证不可变性
public BigDecimal getAmount() {
return amount;
}
public Currency getCurrency() {
return currency;
}
}
4. 领域事件(Domain Event)
领域事件是记录领域内发生的"重要业务事件"的对象,通常以过去时态命名(如OrderCreated、PaymentCompleted),用于捕获状态变更并触发后续业务流程。
核心价值:实现跨上下文的解耦协作;记录业务状态变更(用于审计);支持事件溯源(通过重放事件恢复系统状态)。
Java实例:
// 1. 领域事件定义(不可变)
public class OrderPaidEvent implements DomainEvent, Serializable {
private final OrderId orderId;
private final Money paidAmount;
private final LocalDateTime occurredOn; // 事件发生时间
public OrderPaidEvent(OrderId orderId, Money paidAmount) {
this.orderId = orderId;
this.paidAmount = paidAmount;
this.occurredOn = LocalDateTime.now(); // 创建时固定时间,不可变
}
// getter方法(无setter)
public OrderId getOrderId() {
return orderId;
}
public LocalDateTime getOccurredOn() {
return occurredOn;
}
}
// 2. 聚合根发布事件(事件产生于领域模型)
@Entity
public class Order implements AggregateRoot {
// ... 其他属性和方法
public void pay(Money paidAmount) {
// 支付业务逻辑...
this.status = OrderStatus.PAID;
// 注册领域事件(由框架统一发布)
this.registerDomainEvent(new OrderPaidEvent(this.id, paidAmount));
}
}
// 3. 事件发布器(基础设施层实现)
@Service
public class DomainEventPublisher {
@Autowired
private ApplicationEventPublisher springEventPublisher;
// 发布事件(适配Spring事件机制)
public void publish(DomainEvent event) {
springEventPublisher.publishEvent(event);
}
}
// 4. 事件处理(跨上下文协作,库存上下文监听)
@Component
public class OrderPaidEventHandler {
@Autowired
private InventoryService inventoryService;
// 监听订单支付事件,扣减库存
@EventListener
public void handle(OrderPaidEvent event) {
// 查询订单对应的商品
List<OrderItem> items = orderRepository.findItemsByOrderId(event.getOrderId());
// 扣减库存
for (OrderItem item : items) {
inventoryService.reduceStock(item.getProductId(), item.getQuantity());
}
}
}
5. 领域服务(Domain Service)
领域服务用于处理"不适合放在实体或值对象中"的业务逻辑,尤其是跨多个聚合的协作逻辑或依赖外部系统的业务逻辑。领域服务是领域模型的一部分,仅包含业务逻辑,不关心技术细节。
与应用服务的区别:
- 领域服务:处理核心业务逻辑(如资金转账、运费计算),属于领域层;
- 应用服务:协调整个用例流程(如"用户下单"用例),处理事务、安全等技术细节,属于应用层。
Java实例:资金转账领域服务(跨两个账户聚合)
// 领域服务:资金转账(跨账户聚合的业务逻辑)
@Service
public class TransferDomainService {
private final AccountRepository accountRepository;
// 构造器注入(依赖抽象,不依赖具体实现)
public TransferDomainService(AccountRepository accountRepository) {
this.accountRepository = accountRepository;
}
// 核心业务逻辑:转账
public void transfer(AccountId fromAccountId, AccountId toAccountId, Money amount) {
// 1. 获取两个聚合根
Account fromAccount = accountRepository.findById(fromAccountId)
.orElseThrow(() -> new AccountNotFoundException("转出账户不存在"));
Account toAccount = accountRepository.findById(toAccountId)
.orElseThrow(() -> new AccountNotFoundException("转入账户不存在"));
// 2. 业务规则校验
if (fromAccount.getBalance().compareTo(amount) < 0) {
throw new InsufficientBalanceException("余额不足");
}
// 3. 跨聚合协作(通过聚合根业务方法)
fromAccount.deductBalance(amount); // 账户1扣减余额
toAccount.addBalance(amount); // 账户2增加余额
// 4. 发布领域事件
fromAccount.registerDomainEvent(new MoneyTransferredEvent(fromAccountId, toAccountId, amount));
}
}
// 应用服务:转账用例(协调流程,处理事务)
@Service
@Transactional
public class TransferApplicationService {
private final TransferDomainService transferDomainService;
public TransferApplicationService(TransferDomainService transferDomainService) {
this.transferDomainService = transferDomainService;
}
// 处理外部请求,转换为领域对象,调用领域服务
public void processTransfer(TransferCommand command) {
// 转换DTO为领域对象
AccountId fromAccountId = AccountId.of(command.getFromAccountId());
AccountId toAccountId = AccountId.of(command.getToAccountId());
Money amount = Money.of(new BigDecimal(command.getAmount()), Currency.CNY);
// 调用领域服务处理核心逻辑
transferDomainService.transfer(fromAccountId, toAccountId, amount);
}
}
6. 仓储(Repository)
仓储是领域层与基础设施层之间的桥梁,定义了聚合根的持久化接口,隔离领域模型与数据存储细节。领域层通过仓储接口操作数据,基础设施层实现接口(如基于MyBatis、JPA的实现)。
核心原则:仓储仅操作聚合根,不直接操作聚合内的子对象;仓储方法应体现业务语义(如findByUserId而非findByUserIdAndStatus)。
Java实例:
// 领域层:仓储接口(抽象,不依赖技术)
public interface OrderRepository {
// 业务语义化方法
Order findById(OrderId orderId);
List<Order> findByUserIdAndStatus(UserId userId, OrderStatus status);
void save(Order order); // 保存聚合根(自动持久化聚合内对象)
void delete(Order order);
}
// 基础设施层:仓储实现(基于MyBatis,技术细节隔离)
@Repository
public class OrderRepositoryImpl implements OrderRepository {
@Autowired
private OrderMapper orderMapper;
@Autowired
private OrderItemMapper orderItemMapper;
@Override
public Order findById(OrderId orderId) {
// 1. 查询聚合根
OrderDO orderDO = orderMapper.selectById(orderId.getId());
if (orderDO == null) {
return null;
}
// 2. 查询聚合内子对象(订单项)
List<OrderItemDO> itemDOs = orderItemMapper.selectByOrderId(orderId.getId());
// 3. 转换为领域对象
Order order = convertToDomain(orderDO);
order.setItems(convertItemsToDomain(itemDOs));
return order;
}
@Override
public void save(Order order) {
// 保存聚合根和聚合内对象(事务由应用层控制)
OrderDO orderDO = convertToDO(order);
orderMapper.insert(orderDO);
for (OrderItem item : order.getItems()) {
orderItemMapper.insert(convertItemToDO(item, order.getId()));
}
}
// DO与领域对象的转换方法(隔离数据模型与领域模型)
private Order convertToDomain(OrderDO orderDO) {
// 转换逻辑...
}
}
四、实战中如何使用DDD?
DDD的实战落地遵循"战略设计→战术设计→架构实现→迭代优化"的流程,核心是"从业务出发,逐步落地到技术",以下是具体步骤和实践要点:
4.1 第一步:战略设计(定边界)
战略设计的目标是明确业务边界,为技术落地奠定基础,核心步骤如下:
1. 建立统一语言(Ubiquitous Language)
组织业务专家与技术团队共同梳理业务流程,提炼核心术语(如"空间""租赁订单""合同"),确保团队对术语的理解一致。统一语言将贯穿建模、开发全流程,避免沟通歧义。
实践方法:召开需求研讨会,用业务专家的语言描述流程,将术语记录到"术语表",并同步到代码(如类名、方法名使用统一语言)。
2. 识别领域与子域
基于业务范围梳理整体领域,再按"业务价值优先级"划分核心子域、支撑子域和通用子域。例如:智慧园区系统的核心子域是"空间管理"和"招商租赁",支撑子域是"合同管理",通用子域是"用户认证"。
实践要点:核心子域需投入最优资源,通用子域优先使用现成组件(如用户认证用Spring Security),避免重复开发。
3. 划分界限上下文
通过"事件风暴(Event Storming)"工作坊识别业务事件、命令和领域对象,按"事件流紧密度"和"术语一致性"划分上下文。例如:"空间状态变更""空间分配"等事件属于"空间管理上下文";"租赁订单创建""订单支付"属于"租赁交易上下文"。
实践方法:用便利贴在白板上标注事件、命令、对象,将关联紧密的元素归为一类,形成上下文边界;最终通过包结构或微服务边界落地。
4. 定义上下文映射关系
分析各上下文之间的交互场景,选择合适的映射模式(如共享内核、防腐层、事件发布/订阅)。例如:空间管理上下文与租赁交易上下文通过"事件发布/订阅"协作(空间状态变更后发布事件,租赁上下文监听事件更新订单状态)。
4.2 第二步:战术设计(建模型)
战术设计在每个界限上下文内进行,核心是构建领域模型,步骤如下:
1. 识别聚合与聚合根
基于业务流程识别紧密关联的领域对象,形成聚合;选择聚合中"最核心、最能代表聚合身份"的对象作为聚合根。例如:订单聚合的聚合根是"订单",包含订单项、配送信息等子对象。
实践要点:聚合应"小而内聚",避免过大的聚合导致事务复杂;聚合之间通过ID关联,而非直接引用。
2. 区分实体与值对象
对聚合内的对象,按"是否需要唯一标识"区分实体和值对象。例如:"用户""订单"是实体(有唯一ID);"地址""金额"是值对象(无ID,属性定义身份)。
实践要点:值对象尽量设计为不可变,避免并发问题;实体的业务行为内聚于自身,避免外部直接修改属性。
3. 设计领域服务
梳理跨聚合的业务逻辑或不适合放在实体中的逻辑,设计领域服务。例如:"资金转账"涉及两个账户聚合,适合用领域服务实现;"运费计算"依赖商品重量、目的地等多个因素,也适合用领域服务。
实践要点:领域服务是无状态的,仅包含业务逻辑;依赖通过构造器注入,确保可测试性。
4. 识别领域事件
梳理业务流程中的关键状态变更,定义领域事件。例如:订单创建、支付完成、库存扣减等重要事件,需发布并触发后续流程。
实践要点:事件命名使用过去时态;事件应包含完整的业务上下文(如订单ID、操作金额);事件发布后不可修改。
5. 定义仓储接口
为每个聚合根设计仓储接口,定义业务语义化的查询和保存方法。例如:OrderRepository的findByUserIdAndStatus方法,而非通用的findBy条件方法。
4.3 第三步:架构实现(落地代码)
DDD推荐采用"四层架构"落地,各层职责清晰,依赖关系严格,确保领域层的纯洁性。
1. 四层架构职责划分
|-----------------------|-------------------------------|---------------------------|-------------------|
| 架构分层 | 核心职责 | 典型组件 | 依赖原则 |
| 接口层(User Interface) | 接收外部请求,转换参数,返回响应;不处理业务逻辑 | Controller、RPC接口、DTO转换器 | 仅依赖应用层 |
| 应用层(Application) | 协作用例流程,管理事务,调用领域层;不包含核心业务规则 | 应用服务、命令对象(Command)、事件发布协调 | 依赖领域层,通过接口依赖基础设施层 |
| 领域层(Domain) | 核心业务逻辑,包含聚合根、实体、值对象、领域服务、仓储接口 | 聚合根、实体、值对象、领域服务、仓储接口、领域事件 | 不依赖任何其他层(核心稳定层) |
| 基础设施层(Infrastructure) | 实现领域层接口,提供技术支撑(持久化、消息、外部集成) | 仓储实现、消息队列、外部服务适配器、数据库连接 | 依赖领域层,为其他层提供服务 |
2. 代码组织示例(包结构)
com.smartpark (项目根包)
├─ spacecontext (空间管理上下文,按上下文划分包)
│ ├─ interface (接口层)
│ │ ├─ controller (控制器)
│ │ └─ dto (请求/响应DTO)
│ ├─ application (应用层)
│ │ ├─ service (应用服务)
│ │ └─ command (命令对象)
│ ├─ domain (领域层)
│ │ ├─ model (领域模型)
│ │ │ ├─ Space.java (聚合根)
│ │ │ ├─ SpaceStatus.java (状态枚举)
│ │ │ └─ SpaceRepository.java (仓储接口)
│ │ ├─ service (领域服务)
│ │ └─ event (领域事件)
│ └─ infrastructure (基础设施层)
│ ├─ repository (仓储实现)
│ └─ adapter (外部服务适配器)
├─ leasingcontext (租赁交易上下文,结构同上)
└─ sharedkernel (共享内核,通用模型)
4.4 第四步:迭代优化
DDD不是"一次性设计完成",而是伴随业务发展持续迭代的过程:
- 业务变更时,先更新领域模型,再同步调整技术实现;
- 定期回顾模型与业务的匹配度,重构不合理的聚合或上下文边界;
- 通过测试验证领域模型的正确性(领域层可独立测试,无需依赖技术框架)。
4.5 实战注意事项
- 避免过度设计:简单CRUD系统无需使用DDD(如报表系统),否则会导致"类爆炸",降低开发效率;
- 渐进式落地:不要试图一次性将整个系统DDD化,可选择核心复杂子域(如电商的订单域)作为试点,再逐步推广;
- 团队共识是关键:确保业务和技术团队对统一语言、上下文边界有共同理解,避免"技术自嗨";
- 依赖倒置原则:领域层定义抽象接口,基础设施层实现,避免