一文学会DDD领域驱动设计

相应的设计思想已全部蒸馏为agent skills:https://gitee.com/zrclass/ddd-spec-skills

一、为什么需要 DDD?

想象你在开发一个电商平台。

产品经理说:"用户下单。"

运营说:"用户领券。"

商家说:"用户退款。"

客服说:"用户投诉。"

仓配团队说:"用户催发货。"

乍一看,大家都在说"用户"和"订单"。

但如果你仔细听,会发现他们虽然用了同样的词,关心的却不是同一件事。

  • 产品经理说"用户下单",关心的是交易流程:选商品、下单、支付、取消订单。
  • 运营说"用户领券",关心的是营销转化:活动规则、优惠门槛、券是否可用。
  • 商家说"用户退款",关心的是售后处理:退什么、退多少、商品是否退回、责任由谁承担。
  • 客服说"用户投诉",关心的是问题处理过程:投诉原因、处理记录、工单状态、最终答复。
  • 仓配团队说"用户催发货",关心的是履约流程:仓库是否出库、快递是否揽收、物流进度是否异常。

看起来大家都在讨论"订单",但其实:

  • 交易场景里,订单是一笔购买行为。
  • 履约场景里,订单是一项发货和配送任务。
  • 售后场景里,订单是退款、退货、换货处理的依据。
  • 客服场景里,订单是用户问题的上下文。
  • 营销场景里,订单还是满减、优惠券、会员折扣是否生效的结算载体。

也就是说,同样叫"订单",在不同业务场景里,含义、属性、规则、操作都不一样。

如果你图省事,整个系统只放一个 Order 类,时间一久就会变成这样:

java 复制代码
class Order {
    // 交易相关
    BigDecimal totalAmount;
    PaymentStatus paymentStatus;
    String cancelReason;

    // 履约相关
    ShipmentStatus shipmentStatus;
    String warehouseCode;
    String trackingNumber;
    LocalDateTime deliveredAt;

    // 售后相关
    RefundStatus refundStatus;
    ReturnStatus returnStatus;
    String refundReason;
    String responsibilityType;

    // 客服相关
    ComplaintStatus complaintStatus;
    String serviceRemark;
    String ticketNo;

    // 营销相关
    CouponId couponId;
    BigDecimal discountAmount;
    String promotionType;
}

这时问题就来了:

  • Order 越来越胖,什么都往里塞。
  • 谁都能改订单状态,状态到处乱跳。
  • 一个需求变化,可能牵动交易、营销、履约、售后、客服一大片代码。
  • 开发和业务虽然都在说"订单",但说的根本不是同一个东西。

所以 DDD 要解决的核心问题不是"代码怎么分层"这么简单,而是:

当同一个词在不同业务场景里含义会变化时,怎么划清边界,让每个模型只负责自己该负责的业务。

一句话总结:

DDD 关心的不是"大家都在说订单",而是"大家说的虽然都是订单,但其实不是同一个订单"。


二、DDD 到底是什么?

DDD 是 Domain-Driven Design ,中文叫领域驱动设计

先把"领域"说成人话:

领域,就是你要解决的那类业务问题。

对电商平台来说,"做一个电商平台"太大了,里面其实包含很多不同问题:

  • 用户怎么浏览商品、加购物车、下单?
  • 订单怎么支付、取消、拆单?
  • 仓库怎么拣货、发货、更新物流?
  • 用户怎么退款、退货、换货?
  • 营销活动怎么满减、发券、核销?
  • 客服怎么处理投诉、催单、补偿?

这些都属于电商平台,但它们不是同一个问题。

DDD 的核心思想就是:

不要用一个大而全的模型去硬扛所有问题,而是先识别不同业务问题的边界,再分别建模。

所以 DDD 不是先问:

  • Controller 怎么写?
  • 数据库怎么分表?
  • 用 MySQL 还是 Redis?

而是先问:

  • 当前到底在解决什么业务问题?
  • "订单"在这个场景里是什么意思?
  • 哪些规则必须绑在一起保证?
  • 哪些概念虽然同名,其实不是同一个模型?

三、统一语言(Ubiquitous Language)

3.1 什么叫统一语言?

统一语言的意思是:

开发、产品、测试、运营、商家、客服,在同一个业务场景里,要用同一套词,而且这些词的含义要一致。

这不是简单的"变量名起得好看",而是:

  • 业务怎么说,代码就尽量怎么表达。
  • 一个词在当前上下文里,含义必须稳定。
  • 讨论、文档、代码、接口最好说的是同一种语言。

3.2 电商平台里的例子

如果业务说"提交订单",代码最好别写成:

java 复制代码
createRecord()

而应该写成:

java 复制代码
placeOrder()

如果业务说"支付订单",不要只写:

java 复制代码
updateStatus()

而要写成:

java 复制代码
payOrder()

如果业务说"申请退款",就明确叫:

java 复制代码
applyRefund()

如果业务说"仓库发货",就写:

java 复制代码
shipOrder()

如果业务说"发放优惠券",就写:

java 复制代码
issueCoupon()

3.3 统一语言为什么重要?

因为模糊词最容易让系统失控。

比如一个 close() 方法,谁也不知道它到底是:

  • 关闭交易订单?
  • 关闭售后单?
  • 关闭客服工单?
  • 关闭营销活动?

统一语言的目标,就是让这些词在当前场景里说得清清楚楚。

统一语言不是让全公司只说一套词,而是在每个边界内,把词说准。


四、限界上下文(Bounded Context)

4.1 什么是限界上下文?

你可以把它理解成:

某个模型成立的边界。

也就是说:

  • 在这个边界里,一个词的含义是统一的。
  • 出了这个边界,同一个词可以有别的含义。

4.2 用电商平台理解最合适

电商平台里,"订单"这个词就很典型。

在交易上下文里

"订单"表示一次购买。

它关心的是:

  • 买了什么
  • 总价多少
  • 支付了没有
  • 能不能取消
java 复制代码
public class TradeOrder {
    private OrderId orderId;
    private UserId userId;
    private List<OrderLine> items;
    private Money totalAmount;
    private PaymentStatus paymentStatus;

    public void place() { ... }
    public void pay() { ... }
    public void cancel() { ... }
}
在履约上下文里

"订单"表示一次发货任务。

它关心的是:

  • 是否拆单
  • 仓库是否出库
  • 快递单号是什么
  • 是否签收
java 复制代码
public class FulfillmentOrder {
    private OrderId orderId;
    private WarehouseId warehouseId;
    private ShipmentStatus shipmentStatus;
    private TrackingNumber trackingNumber;

    public void allocateStock() { ... }
    public void ship() { ... }
    public void confirmDelivery() { ... }
}
在售后上下文里

"订单"表示一次售后处理依据。

它关心的是:

  • 是否申请退款
  • 是否退货
  • 原因是什么
  • 处理进度到哪了
java 复制代码
public class AfterSaleCase {
    private OrderId orderId;
    private RefundStatus refundStatus;
    private ReturnStatus returnStatus;
    private String reason;

    public void applyRefund(String reason) { ... }
    public void requestReturn(String reason) { ... }
}
在客服上下文里

"订单"表示一次用户问题的上下文。

它关心的是:

  • 用户咨询了什么
  • 客服怎么处理
  • 工单状态如何
  • 是否投诉完成
java 复制代码
public class ServiceTicket {
    private TicketId ticketId;
    private OrderId orderId;
    private TicketStatus ticketStatus;
    private String serviceRemark;

    public void openTicket(String content) { ... }
    public void closeTicket() { ... }
}
在营销上下文里

"订单"表示优惠规则结算的载体。

它关心的是:

  • 是否满足满减门槛
  • 哪张券可用
  • 折扣怎么计算
  • 活动是否叠加
java 复制代码
public class PromotionOrder {
    private OrderId orderId;
    private Money originalAmount;
    private Money discountAmount;
    private CouponId couponId;

    public void applyCoupon(CouponId couponId) { ... }
    public void calculateDiscount() { ... }
}

4.3 限界上下文的核心价值

DDD 想表达的不是"多建几个类",而是:

交易、履约、售后、客服、营销虽然都和订单有关,但它们不是同一个业务问题,所以不该强行共享一个模型。

你可以把它理解成:

  • 交易上下文有自己的 TradeOrder
  • 履约上下文有自己的 FulfillmentOrder
  • 售后上下文有自己的 AfterSaleCase
  • 客服上下文有自己的 ServiceTicket
  • 营销上下文有自己的 PromotionOrder

同样叫"订单",但每个边界里都只保留自己关心的部分。


五、实体(Entity)与值对象(Value Object)

5.1 实体:重点是"它是谁"

在电商平台里,下面这些通常是实体:

  • 用户
  • 商品
  • 商家
  • 交易订单
  • 售后单
  • 优惠券

因为这些对象都有明确身份,要长期跟踪。

比如一个用户改了手机号,还是同一个用户;

一个订单改了收货地址,还是同一个订单。

java 复制代码
public class User {
    private UserId userId;
    private String nickname;
    private PhoneNumber phoneNumber;

    public void changePhoneNumber(PhoneNumber newPhoneNumber) {
        this.phoneNumber = newPhoneNumber;
    }
}

实体的特点:

  • 有唯一标识
  • 有生命周期
  • 重点在身份连续性

5.2 值对象:重点是"它是什么值"

电商平台里常见的值对象有:

  • Money
  • Address
  • PhoneNumber
  • SkuCode
  • TimeRange

这些对象一般不需要单独追踪身份,只关心值本身。

java 复制代码
public final class Address {
    private final String province;
    private final String city;
    private final String detail;

    public Address(String province, String city, String detail) {
        this.province = province;
        this.city = city;
        this.detail = detail;
    }
}

5.3 怎么判断?

问自己一句:

业务上,我需要持续追踪"它是谁"吗?

  • 需要:实体
  • 不需要,只关心值:值对象

5.4 为什么值对象很有用?

因为它能让代码更像业务。

比如:

java 复制代码
public void placeOrder(String address, BigDecimal amount, String phone) { ... }

不如:

java 复制代码
public void placeOrder(Address address, Money amount, PhoneNumber phone) { ... }

后者更清楚,也更容易做校验。


六、聚合与聚合根(Aggregate / Aggregate Root)

6.1 先讲人话

聚合是一组需要一起维护一致性的对象。
聚合根是这组对象对外的总入口。

6.2 用交易订单举例

一个交易订单里,通常会包含:

  • 订单本身
  • 多个订单项
  • 收货地址
  • 价格信息

这些对象不是独立乱改的,因为它们之间有规则:

  • 已取消订单不能再加商品
  • 已支付订单不能改金额
  • 已完成订单不能改地址

所以应该通过订单这个聚合根统一修改。

java 复制代码
// 聚合根:TradeOrder
public class TradeOrder {
    private OrderId orderId;
    private List<OrderLine> items;   // 聚合内部对象
    private Address deliveryAddress; // 聚合内部对象
    private TradeOrderStatus status;

    public void addItem(ProductId productId, int quantity) {
        if (status == TradeOrderStatus.CANCELLED) {
            throw new IllegalStateException("已取消订单不能添加商品");
        }
        items.add(new OrderLine(productId, quantity));
    }

    public void changeAddress(Address newAddress) {
        if (status == TradeOrderStatus.COMPLETED) {
            throw new IllegalStateException("已完成订单不能修改地址");
        }
        this.deliveryAddress = newAddress;
    }
}

聚合是一组要一起守规则的对象;聚合根是这组对象对外的总入口。

放到这个例子里就是:

  • 聚合:交易订单这一整组对象
  • 聚合根:TradeOrder

6.3 为什么要这么做?

因为如果外部代码随便拿内部对象乱改:

java 复制代码
order.getItems().add(...);
order.setStatus(...);
order.setDeliveryAddress(...);

业务规则就守不住了。

所以更准确的说法是:

聚合根是外部修改聚合内部状态的唯一业务入口。

6.4 聚合的几个实用原则

  1. 外部通过聚合根执行业务修改。
  2. 聚合尽量小。
  3. 一个事务应尽量只修改一个聚合。
  4. 跨聚合协作优先考虑事件或最终一致性。

七、领域事件(Domain Event)

7.1 什么是领域事件?

领域事件表示:

业务上已经发生的重要事实。

电商平台里常见的领域事件有:

  • OrderPlaced
  • PaymentCompleted
  • OrderShipped
  • RefundApplied
  • CouponIssued

7.2 它有什么用?

比如用户完成下单后,可能会发生很多后续动作:

  • 交易上下文记录订单已创建
  • 营销上下文冻结或核销优惠券
  • 履约上下文创建发货任务
  • 通知系统发送下单成功消息

如果全写在一个对象里,会耦合得很厉害。

领域事件的作用就是:

让"事情发生了"与"谁来处理这个事情"解耦。

7.3 示例

java 复制代码
public class OrderPlaced {
    private OrderId orderId;
    private UserId userId;
    private LocalDateTime occurredOn;
}

交易上下文发布 OrderPlaced,履约上下文监听后创建自己的 FulfillmentOrder

7.4 一个重要提醒

领域事件不等于事件溯源。

  • 领域事件:表达业务事实
  • 事件溯源:用事件重建状态

很多系统会用领域事件,但并不会做事件溯源。


八、领域服务(Domain Service)

8.1 什么逻辑适合放领域服务?

如果一段业务逻辑:

  • 不自然属于某个实体
  • 也不适合放在值对象里
  • 但它本身又是明确的业务规则

那就适合做成领域服务。

8.2 电商平台里的自然例子:促销价格计算

促销价格通常和这些因素有关:

  • 商品原价
  • 优惠券
  • 满减活动
  • 会员折扣
  • 活动叠加规则

这个逻辑很难说完全属于 TradeOrderCouponProduct 某一个实体。

那就可以抽成领域服务:

java 复制代码
public class PromotionPricingService {

    public Money calculatePayableAmount(
            Money originalAmount,
            Coupon coupon,
            PromotionRule rule,
            MembershipLevel membershipLevel) {
        ...
    }
}

8.3 注意别把所有东西都塞进 Service

DDD 不是"所有业务逻辑都写进 XXXService"。

更合理的原则是:

  • 能放进实体,就放进实体
  • 能放进值对象,就放进值对象
  • 真正不属于任何单一对象的业务规则,再放进领域服务

九、应用服务(Application Service)

9.1 应用服务负责什么?

应用服务不负责核心业务规则,它负责:

  • 接收请求
  • 参数转换
  • 加载聚合
  • 调用领域对象
  • 保存结果
  • 返回响应

9.2 下单流程示例

java 复制代码
public class TradeApplicationService {

    public void placeOrder(PlaceOrderCommand command) {
        UserId userId = new UserId(command.getUserId());
        Address address = new Address(
                command.getProvince(),
                command.getCity(),
                command.getDetail());

        TradeOrder order = TradeOrder.create(userId, command.getItems(), address);

        tradeOrderRepository.save(order);
    }
}

这里它做的是"流程编排",不是在定义"什么叫下单成功"。

9.3 和领域服务的区别

一句话区分:

  • 应用服务:这件事怎么走完
  • 领域服务:这件事在业务上怎么算成立

十、仓储(Repository)

仓储可以理解为:

聚合的存取窗口。

领域层只关心:

  • 怎么把聚合取出来
  • 怎么把聚合存回去

不关心底层是数据库、缓存还是别的技术。

java 复制代码
public interface TradeOrderRepository {
    TradeOrder orderOfId(OrderId orderId);
    void save(TradeOrder order);
}

一个重要提醒

仓储通常是面向聚合根的,不是每个对象都要建一个 Repository。

不要变成这样:

  • OrderLineRepository
  • AddressRepository
  • CouponRuleRepository

不然聚合边界就散了。


十一、防腐层(Anti-Corruption Layer, ACL)

11.1 它解决什么问题?

当你的电商平台要接第三方系统时,比如:

  • 第三方支付平台
  • 第三方物流平台
  • 第三方会员平台

最怕的是这些外部系统的模型直接污染你的领域模型。

11.2 用物流场景举例

第三方物流平台可能返回一个很技术化的 DTO:

java 复制代码
class LogisticsGatewayResponse {
    String code;
    String waybillNo;
    String nodeStatus;
    String rawMessage;
}

但在履约上下文里,你真正关心的可能只是:

java 复制代码
class ShipmentTracking {
    TrackingNumber trackingNumber;
    ShipmentStatus shipmentStatus;
}

这时就应该通过 ACL 做一层翻译,而不是让物流平台的结构直接进领域层。

11.3 ACL 的核心

把外部模型翻译成内部模型,保护自己的领域不被污染。


十二、DDD 的分层架构

12.1 四层架构

包约定 电商示例
接口层 resource/port/adapter/ TradeOrderResource, FulfillmentOrderAdapter
应用层 application/ TradeOrderApplicationService, PlaceOrderCommand, TradeOrderData
领域层 domain.model/ TradeOrder, OrderId, Money, OrderPlaced, TradeOrderRepository(接口), OrderPricingService(接口)
基础设施层 infrastructure/port/adapter/ HibernateTradeOrderRepository, RabbitMQOrderPlacedListener, LogisticsGatewayAdapter

12.2 依赖方向

最关键的规则:

领域层不依赖外层技术实现。

text 复制代码
接口层 → 应用层 → 领域层
基础设施层 → 领域层

这意味着:

  • 领域层定义仓储接口,基础设施层实现它
  • 领域层定义领域服务接口,基础设施层适配外部系统
  • 领域对象不应该出现 @Entity@Autowired@Component 等框架注解

12.3 交易上下文包结构(传统四层 + Hibernate)

text 复制代码
com.ecommerce.trade/
  domain.model.order/           → TradeOrder, OrderLine, OrderPlaced, TradeOrderRepository(接口)
  domain.model.pricing/         → OrderPricingService(接口), Money, CouponId
  application/                  → TradeOrderApplicationService, command/, representation/
  infrastructure.persistence/   → HibernateTradeOrderRepository
  infrastructure.messaging/     → RabbitMQOrderPlacedListener
  resource/                     → TradeOrderResource

12.4 履约上下文包结构(六边形/端口适配器 + LevelDB)

text 复制代码
com.ecommerce.fulfillment/
  domain.model.fulfillment/     → FulfillmentOrder, ShipmentStatus, FulfillmentOrderRepository(接口)
  domain.model.tracking/        → ShipmentTrackingService(接口)
  application/                  → FulfillmentApplicationService
  port.adapter.persistence/     → LevelDBFulfillmentOrderRepository
  port.adapter.messaging/       → RabbitMQOrderPlacedListener
  port.adapter.service/         → LogisticsGatewayAdapter, TranslatingShipmentService

12.5 仓储接口与实现的分离

java 复制代码
// 领域层 --- 仓储接口(不依赖任何框架)
package com.ecommerce.trade.domain.model.order;
public interface TradeOrderRepository {
    void add(TradeOrder aTradeOrder);
    void remove(TradeOrder aTradeOrder);
    TradeOrder nextIdentity();
    TradeOrder orderOfId(OrderId anOrderId);
}

// 基础设施层 --- Hibernate 实现(ORM 模式)
package com.ecommerce.trade.infrastructure.persistence;
public class HibernateTradeOrderRepository extends AbstractHibernateSession
    implements TradeOrderRepository {
    @Override
    public void add(TradeOrder aTradeOrder) {
        this.session().save(aTradeOrder);
    }
}

// 基础设施层 --- EventStore 实现(事件溯源模式)
package com.ecommerce.trade.infrastructure.persistence;
public class EventStoreTradeOrderRepository implements TradeOrderRepository {
    public TradeOrder orderOfId(OrderId anOrderId) {
        EventStreamId streamId = new EventStreamId("TradeOrder:" + anOrderId.id());
        EventStream eventStream = this.eventStore().eventStreamSince(streamId, 0);
        return new TradeOrder(eventStream.events(), eventStream.version());
    }
}

12.6 反模式 --- JPA 注解污染领域模型

java 复制代码
// ❌ 反模式 --- 领域层依赖框架
@Entity
@Table(name="TRADE_ORDER")
public class TradeOrder extends ConcurrencySafeEntity {
    @Id  // ❌ 领域对象不应知道持久化细节
    private Long id;
}

12.7 领域服务 --- 跨聚合逻辑

java 复制代码
// OrderPricingService --- 促销定价(跨聚合逻辑)
public interface OrderPricingService {
    Money calculatePayableAmount(
        Money originalAmount,
        Coupon coupon,
        PromotionRule rule,
        MembershipLevel membershipLevel);
}

12.8 DomainRegistry 与依赖注入

java 复制代码
// 方式1:DomainRegistry(服务定位器模式)
public class TradeOrder extends ConcurrencySafeEntity {  // ConcurrencySafeEntity 提供乐观并发控制
    protected Money calculateDiscount(Coupon coupon) {
        return DomainRegistry.orderPricingService()
            .calculatePayableAmount(this.totalAmount(), coupon, null, null);
    }
}

// 方式2:构造函数注入(更推荐)
public class TradeOrder extends ConcurrencySafeEntity {
    private final OrderPricingService pricingService;

    public TradeOrder(..., OrderPricingService pricingService) {
        this.pricingService = pricingService;
    }
}

// ❌ 反模式 --- 领域模型直接依赖 Spring
@Component  // ❌ 领域对象不应是 Spring Bean
public class TradeOrder extends ConcurrencySafeEntity {
    @Autowired  // ❌ 领域对象不应使用字段注入
    private OrderPricingService pricingService;
}

12.9 应用服务 --- 薄编排层

java 复制代码
// 应用服务不做业务判断,只编排领域对象
public void placeOrder(PlaceOrderCommand command) {
    OrderId orderId = this.tradeOrderRepository().nextIdentity();
    TradeOrder order = new TradeOrder(
        orderId,
        command.getUserId(),
        command.getItems(),
        new Address(command.getProvince(), command.getCity(), command.getDetail()));
    this.tradeOrderRepository().add(order);
}

十三、进阶:事件溯源(Event Sourcing)与 CQRS

这部分是进阶,不是入门必选。

13.1 事件溯源

传统做法通常只保存订单当前状态:

text 复制代码
订单:已支付,待发货,实付 199 元

事件溯源保存的是订单经历过什么:

text 复制代码
1. OrderPlaced
2. CouponApplied
3. PaymentCompleted
4. OrderShipped

当前状态是通过回放事件得到的。

适合:

  • 审计要求高
  • 希望完整保留状态变化历史
  • 领域天然事件驱动

不适合:

  • 业务简单
  • 团队经验不足
  • 只是普通 CRUD

13.2 CQRS

CQRS 的核心是:

读和写用不同模型。

写模型 --- 命令端
java 复制代码
// TradeOrderApplicationService --- 写模型入口
public class TradeOrderApplicationService {
    public void placeOrder(PlaceOrderCommand command) {
        OrderId orderId = this.tradeOrderRepository().nextIdentity();
        TradeOrder order = new TradeOrder(
            orderId,
            command.getUserId(),
            command.getItems(),
            new Address(command.getProvince(), command.getCity(), command.getDetail()));
        this.tradeOrderRepository().add(order);  // 追加事件到事件存储
    }

    public void payOrder(PayOrderCommand command) {
        TradeOrder order = this.tradeOrderRepository().orderOfId(new OrderId(command.getOrderId()));
        order.pay();  // 业务规则在聚合根内
        this.tradeOrderRepository().add(order);  // 追加事件到事件存储
    }
}
读模型 --- 查询端
java 复制代码
// TradeOrderQueryService --- 读模型入口
public class TradeOrderQueryService extends AbstractQueryService {
    public TradeOrderData orderOfId(String orderId) {
        // 纯 JDBC 查询投影表,不访问领域模型
        // 返回轻量级 TradeOrderData,不是 TradeOrder 实体
    }
}
投影 --- 事件到读模型
java 复制代码
// MySQLTradeOrderProjection --- 监听事件并更新读模型
public class MySQLTradeOrderProjection extends AbstractProjection {
    public void handleEvent(OrderPlaced event) {
        // 收到 OrderPlaced 事件后,在投影表中插入记录
        this.insertOrderRow(event);
    }

    public void handleEvent(PaymentCompleted event) {
        // 收到 PaymentCompleted 事件后,更新投影表状态
        this.updateOrderStatus(event.orderId(), "PAID");
    }

    public void handleEvent(OrderShipped event) {
        this.updateOrderStatus(event.orderId(), "SHIPPED");
    }
}

注意:

CQRS 不等于一定要分库分服务。

单体里也能先做逻辑上的读写分离。关键是写模型操作聚合、读模型查投影表,两者模型独立。

13.3 事件溯源代码示例

前面讲了事件溯源的概念,这里用电商订单给出核心代码示例。

事件溯源聚合根
java 复制代码
// TradeOrder 事件溯源聚合根
public class TradeOrder extends EventSourcedRootEntity {

    // 无参构造函数:初始化事件处理基础设施
    private TradeOrder() {
        super();
    }

    // 从命令创建:通过 apply() 发布事件
    public TradeOrder(OrderId orderId, UserId userId,
                      List<OrderLine> items, Address deliveryAddress) {
        this();
        this.apply(new OrderPlaced(orderId, userId, items, deliveryAddress));
    }

    // 从事件流重建:委托给 EventSourcedRootEntity
    public TradeOrder(List<DomainEvent> eventStream, int streamVersion) {
        super(eventStream, streamVersion);  // 重放所有事件
    }

    // when() 方法从事件重建状态 --- 绝不直接设置状态
    protected void when(OrderPlaced event) {
        this.setOrderId(event.orderId());
        this.setUserId(event.userId());
        this.setItems(event.items());
        this.setDeliveryAddress(event.deliveryAddress());
        this.setStatus(TradeOrderStatus.PLACED);
    }

    protected void when(PaymentCompleted event) {
        this.setStatus(TradeOrderStatus.PAID);
    }

    protected void when(OrderShipped event) {
        this.setStatus(TradeOrderStatus.SHIPPED);
    }

    protected void when(OrderDelivered event) {
        this.setStatus(TradeOrderStatus.DELIVERED);
    }

    protected void when(OrderCancelled event) {
        this.setStatus(TradeOrderStatus.CANCELLED);
    }

    // 命令方法只发布事件,不直接修改状态
    public void pay() {
        this.assertStateTrue(this.status() == TradeOrderStatus.PLACED, "只有已下单的订单才能支付");
        this.apply(new PaymentCompleted(this.orderId()));
        // 注意:没有 this.setStatus(PAID) --- 状态由 when() 修改
    }

    public void cancel(String reason) {
        this.assertStateTrue(this.status() != TradeOrderStatus.DELIVERED, "已发货的订单不能取消");
        this.apply(new OrderCancelled(this.orderId(), reason));
    }
}
事件存储仓储
java 复制代码
// EventStoreTradeOrderRepository --- 通过事件流加载聚合
public class EventStoreTradeOrderRepository implements TradeOrderRepository {
    public TradeOrder orderOfId(OrderId orderId) {
        EventStreamId streamId = new EventStreamId("TradeOrder:" + orderId.id());
        EventStream eventStream = this.eventStore().eventStreamSince(streamId, 0);
        return new TradeOrder(eventStream.events(), eventStream.version());
        // 通过重放所有事件重建 TradeOrder 当前状态
    }
    // save() 实现省略
}

关键区别:

  • 传统 ORM 模式HibernateTradeOrderRepository 直接加载聚合当前状态
  • 事件溯源模式EventStoreTradeOrderRepository 通过重放事件流重建状态

也就是说,事件溯源仓储的 orderOfId() 不是查数据库拿一行记录,而是把该订单的所有历史事件按顺序重放一遍,最终得到当前状态。

13.4 应用服务事务生命周期

在事件溯源架构中,应用服务的每次操作都遵循一个固定的事务生命周期。前面 13.2 的应用服务示例省略了事务管理,这里补全:

java 复制代码
// ApplicationServiceLifeCycle --- 事务生命周期管理
// LevelDBUnitOfWork 是一种具体实现,不同的存储后端有不同的 UnitOfWork 实现
public class ApplicationServiceLifeCycle {
    public static void begin() {
        LevelDBUnitOfWork.start();  // 开始工作单元
        listen();                    // 注册事件监听器(收集聚合产生的领域事件)
    }

    public static void success() {
        LevelDBUnitOfWork.current().commit();  // 提交事件到存储
        publishNotifications();                 // 发布通知给其他上下文
    }

    public static void fail() {
        LevelDBUnitOfWork.current().rollback(); // 回滚,所有事件丢弃
    }
}

使用示例 --- 把 13.2 的 payOrder 方法补上事务生命周期:

java 复制代码
public void payOrder(PayOrderCommand command) {
    ApplicationServiceLifeCycle.begin();
    try {
        TradeOrder order = this.tradeOrderRepository().orderOfId(new OrderId(command.getOrderId()));
        order.pay();
        this.tradeOrderRepository().add(order);
        ApplicationServiceLifeCycle.success();
    } catch (Exception e) {
        ApplicationServiceLifeCycle.fail();
    }
}

三个阶段的职责:

  • begin():开启工作单元,注册事件监听器
  • success():提交事件到存储,发布通知给其他上下文
  • fail():回滚,所有事件丢弃

这个模式确保了业务操作的原子性:要么聚合的所有事件都持久化成功,要么全部回滚,不会出现"改了一半"的中间状态。


十四、什么时候适合用 DDD?

适合:

  • 业务复杂
  • 同一个词在不同场景里含义经常变化
  • 多团队协作
  • 需求变化快
  • 系统要长期演进

不太适合:

  • 纯 CRUD 后台
  • 很小的工具系统
  • 业务规则极少
  • 团队没有精力做业务建模

十五、DDD 落地步骤

如果你要在电商平台里落地 DDD,可以按这个顺序做:

1. 先和业务一起把词说清楚

比如明确:

  • 什么叫下单成功
  • 什么叫支付成功
  • 什么叫发货
  • 什么叫退款通过
  • 什么叫投诉关闭

2. 划分业务边界

把交易、履约、售后、客服、营销拆开。

3. 在每个边界里建模

识别:

  • 哪些是实体
  • 哪些是值对象
  • 哪些组成聚合
  • 会发生哪些领域事件

4. 定义边界之间怎么协作

比如:

  • 交易发布 OrderPlaced
  • 履约监听并创建发货任务
  • 营销根据订单金额核销优惠
  • 售后通过 API 查询交易订单信息

5. 从轻量做起

不要一上来就全套事件溯源 + CQRS + 微服务。

6. 持续演进

DDD 不是一次性画完图,而是随着业务理解不断重构。


十六、常见误区

误区 更准确的理解
DDD 就是多写几个实体和值对象 DDD 的核心是统一语言和限界上下文
DDD 就是微服务 微服务只是可能的落地方式
所有逻辑都放 Service 规则应尽量回到领域对象
一个事务绝不能改多个聚合 更准确是应尽量只改一个聚合
有领域事件就等于事件溯源 两者不是一回事
每个类都要建一个 Repository 仓储通常面向聚合根
DDD 一定更高级 合适比高级更重要

十七、一张图总结

text 复制代码
                领域驱动设计 DDD
                        │
        ┌───────────────┼───────────────┐
        │               │               │
     战略设计         战术设计         架构实现
        │               │               │
  - 统一语言        - 实体            - 分层架构
  - 限界上下文      - 值对象          - 六边形架构
  - 上下文映射      - 聚合            - 事件驱动
  - 防腐层          - 领域事件        - 事件溯源
                    - 领域服务        - CQRS
                    - 仓储

十八、最后一句总结

如果你只记住一句话,那就记住:

DDD 不是教你把代码写复杂,而是教你承认业务的真实边界。

在电商平台里,大家虽然都在说"用户"和"订单",但交易、履约、售后、客服、营销说的其实不是同一个东西。

DDD 做的,就是把这些差异明明白白地建模出来,让代码终于和业务说同一种语言。

相关推荐
rolt12 天前
[幻灯片]分析设计高阶-02结构(2)-202604更新
ddd·架构师·uml·ooad
刀法如飞19 天前
一款Python语言Django框架DDD脚手架,助你快速搭建项目
python·ddd·脚手架
YDS82925 天前
大营销平台 —— 模板方法串联前中置抽奖规则
java·spring boot·ddd
YDS8291 个月前
大营销平台 —— 抽奖规则决策树
java·springboot·ddd
YDS8291 个月前
大营销平台 —— 抽奖前置规则过滤
java·spring boot·ddd
一条咸鱼_SaltyFish1 个月前
DDD 架构重构实践:AI Skills 如何赋能DDD设计与重构
java·人工智能·ai·重构·架构·ddd·领域驱动设计
m0_651593911 个月前
领域驱动设计(DDD)实战指南:如何正确构建复杂系统
java·软件工程·ddd
洛洛呀。2 个月前
DDD架构为何拆分Entity层?从MVC到领域模型的演进之道
架构·mvc·ddd
asom222 个月前
MVC vs DDD
java·mvc·ddd