DDD领域驱动设计的8个核心概念

  |   0 评论   |   54 浏览

为什么你的代码总是改一处、崩一片?因为缺少"领域语言"。

Eric Evans 在 2003 年提出了领域驱动设计(Domain-Driven Design,简称 DDD),它不是一种技术,而是一种通过领域建模来驱动软件设计的方法论

今天,我们来深入理解 DDD 的 8 个核心概念。


一、什么是 DDD?

核心思想:软件的本质是对现实世界的数字化建模

现实世界                软件世界
─────────                ─────────
用户下单     →     Order.create()
商品库存     →     Product.reduceStock()
支付成功     →     Payment.confirm()

问题来了:如果开发者和业务专家说的不是同一种语言?

开发者:这个 Order 实体的 status 字段改成 2
业务:订单"确认"了?那要通知仓库发货啊!
😱 理解偏差,导致系统bug

DDD 的解决方案统一语言(Ubiquitous Language)
- 开发者、业务专家、产品经理使用相同的术语
- 代码直接反映业务概念
- 减少"翻译"带来的信息丢失


二、8种核心概念全景图

8 Key Concepts in DDD


三、领域建模基础

1. Business Entities (业务实体)

核心思想:用模型表达业务概念和知识

什么是实体?
实体是具有唯一标识(ID)的事物,通过ID而非属性来区分。

// ❌ 只关注属性,不是领域思维
public class User {
    private String name;
    private int age;
    private String email;
}

// ✅ 关注身份的实体
@Entity
public class User {
    private UserId id;  // 唯一标识,核心!

    private String name;
    private int age;
    private String email;

    // 即使名字改成"张三",这还是同一个人
    // 因为 ID 没变
}

真实案例:电商订单

// 订单实体
@Entity
public class Order {
    private OrderId id;  // 订单ID

    private CustomerId customerId;
    private List<OrderItem> items;
    private OrderStatus status;
    private Money totalAmount;

    // 领域行为(业务逻辑)
    public void pay(PaymentMethod method) {
        if (this.status != OrderStatus.PENDING) {
            throw new OrderAlreadyPaidException();
        }
        this.status = OrderStatus.PAID;
        // 发布领域事件
        DomainEvents.publish(new OrderPaidEvent(this.id));
    }

    public void ship(Address shippingAddress) {
        if (this.status != OrderStatus.PAID) {
            throw new OrderNotPaidException();
        }
        this.status = OrderStatus.SHIPPED;
        DomainEvents.publish(new OrderShippedEvent(this.id));
    }
}

关键要点
- ✅ 实体有唯一的 ID
- ✅ 实体封装业务行为(不只是数据容器)
- ✅ 实体有生命周期(创建→修改→删除)


2. Model Boundaries (模型边界)

核心思想:用边界来隔离复杂度

为什么需要边界?

想象一个巨型电商系统,如果所有东西都混在一起:

UserService 依赖 OrderService
OrderService 依赖 PaymentService
PaymentService 依赖 InventoryService
InventoryService 依赖 UserService
😱 循环依赖,牵一发而动全身

DDD 的解决方案:限界上下文 (Bounded Context)

销售上下文        库存上下文        物流上下文
─────────        ─────────        ─────────
订单 (Order)     库存 (Stock)      运单 (Shipment)
客户 (Customer)  商品 (Product)    包裹 (Package)

每个上下文有独立的模型和数据!

实践案例:用户的两种身份

// 销售上下文中的"客户"
@Entity
public class Customer {
    private CustomerId id;
    private String name;
    private String shippingAddress;
    private VIPLevel level;  // VIP等级
}

// 营销上下文中的"用户"
@Entity
public class User {
    private UserId id;
    private String email;
    private List<Tag> interests;  // 兴趣标签
    private Date lastLoginTime;
}

// 它们是不同的概念,有独立的数据库表

边界的好处
- ✅ 团队可以并行开发(销售团队、库存团队互不干扰)
- ✅ 微服务拆分的自然边界
- ✅ 降低认知负担(每个上下文关注自己的业务)


3. Aggregation (聚合)

核心思想:把相关对象当作一个单元来管理

问题场景:购物车的一致性

// ❌ 没有聚合概念
class Cart {
    List<CartItem> items;
}

class CartItem {
    Product product;
    int quantity;
}

// 问题:谁能保证下面的操作是原子的?
cart.getItems().add(item1);
cart.getItems().add(item2);
cart.getTotalAmount();  // 可能还没计算完!

聚合根 (Aggregate Root) 来拯救

// ✅ 使用聚合
public class Cart {  // 聚合根
    private CartId id;
    private List<CartItem> items;
    private Money totalAmount;

    // 只能通过聚合根修改内部对象
    public void addItem(Product product, int quantity) {
        CartItem item = findItem(product);
        if (item != null) {
            item.increaseQuantity(quantity);
        } else {
            items.add(new CartItem(product, quantity));
        }
        updateTotalAmount();
    }

    public void removeItem(ProductId productId) {
        items.removeIf(item -> item.getProductId().equals(productId));
        updateTotalAmount();
    }

    // 内部方法,外部不能直接调用
    private void updateTotalAmount() {
        this.totalAmount = items.stream()
            .map(CartItem::getSubTotal)
            .reduce(Money.ZERO, Money::add);
    }
}

// 外界只能通过聚合根操作
cart.addItem(product, 2);  // ✅ 保证一致性
cart.getItems().clear();   // ❌ 编译器不允许!

聚合的设计原则
1. 只通过聚合根访问内部对象
2. 聚合之间通过ID引用,而非对象引用
3. 一次事务只修改一个聚合


4. Entities vs. Value Objects (实体 vs 值对象)

核心区别:有没有身份标识

对比表

维度 实体 (Entity) 值对象 (Value Object)
标识 有 ID 无 ID
相等性 ID 相同即相同 属性相同即相同
可变性 可变 不可变
生命周期 有(创建→修改→删除) 附属于实体
示例 订单、用户 金额、地址、日期

值对象的实践

// ✅ 金额作为值对象
@ValueObject  // 不可变!
public class Money {
    private final BigDecimal amount;
    private final Currency currency;

    public Money(BigDecimal amount, Currency currency) {
        this.amount = amount;
        this.currency = currency;
    }

    public Money add(Money other) {
        if (!this.currency.equals(other.currency)) {
            throw new CurrencyMismatchException();
        }
        return new Money(this.amount.add(other.amount), this.currency);
    }

    // 相等性:所有属性相同才相等
    @Override
    public boolean equals(Object obj) {
        if (!(obj instanceof Money)) return false;
        Money other = (Money) obj;
        return this.amount.equals(other.amount)
            && this.currency.equals(other.currency);
    }
}

// 使用
Money price1 = new Money(new BigDecimal("100"), Currency.CNY);
Money price2 = new Money(new BigDecimal("100"), Currency.CNY);
price1.equals(price2);  // ✅ true(属性相同)

// ✅ 地址作为值对象
@ValueObject
public class Address {
    private final String province;
    private final String city;
    private final String street;
    private final String zipCode;

    // 不可变,没有setter
}

为什么要用值对象?
- ✅ 消除"基本类型偏执"(Primitive Obsession)
- ✅ 代码更安全(不可变)
- ✅ 业务逻辑更清晰("100元"比"100"有意义)


四、领域操作

5. Operational Modeling (操作建模)

核心思想:用专门的对象来操作领域模型

问题:谁负责业务逻辑?

// ❌ 贫血模型(Anemic Model)
@Entity
public class Order {
    private OrderId id;
    private Money totalAmount;
    private OrderStatus status;
    // 只有getter/setter,没有业务逻辑
}

// 业务逻辑散落在Service里
@Service
public class OrderService {
    public void cancelOrder(Order order) {
        if (order.getStatus() == OrderStatus.SHIPPED) {
            throw new Exception("已发货,不能取消");
        }
        if (order.getStatus() == OrderStatus.PAID) {
            // 退款逻辑
            refundService.refund(order.getTotalAmount());
        }
        order.setStatus(OrderStatus.CANCELLED);
    }
}
// 问题:业务逻辑泄露到Service,Order变成数据容器

DDD 的解决方案:领域服务 (Domain Service) + 领域事件

// ✅ 充血模型(Rich Model)
@Entity
public class Order {
    private OrderId id;
    private Money totalAmount;
    private OrderStatus status;
    private List<DomainEvent> events = new ArrayList<>();

    // 业务逻辑回到实体内部
    public void cancel() {
        if (this.status == OrderStatus.SHIPPED) {
            throw new OrderCannotCancelException("已发货,不能取消");
        }

        OrderStatus previousStatus = this.status;
        this.status = OrderStatus.CANCELLED;

        // 记录领域事件
        this.events.add(new OrderCancelledEvent(this.id, previousStatus));
    }

    public List<DomainEvent> getUncommittedEvents() {
        return Collections.unmodifiableList(events);
    }

    public void markEventsAsCommitted() {
        events.clear();
    }
}

// 领域服务处理跨聚合的业务逻辑
@DomainService
public class OrderCancellationService {
    private final OrderRepository orderRepository;
    private final RefundService refundService;

    public void cancelOrder(OrderId orderId) {
        Order order = orderRepository.findById(orderId);

        order.cancel();  // 核心业务逻辑在实体内

        // 处理领域事件
        for (DomainEvent event : order.getUncommittedEvents()) {
            if (event instanceof OrderCancelledEvent) {
                if (((OrderCancelledEvent) event).getPreviousStatus() == OrderStatus.PAID) {
                    refundService.refund(order.getTotalAmount());
                }
            }
            eventPublisher.publish(event);
        }

        order.markEventsAsCommitted();
        orderRepository.save(order);
    }
}

领域事件 (Domain Event) 的好处:
- ✅ 解耦:订单不需要知道退款服务的存在
- ✅ 可追溯:记录了完整的业务流程
- ✅ 易扩展:新增监听器不影响原有逻辑


6. Layering the Architecture (分层架构)

核心思想:用分层来组织复杂的系统

DDD 的四层架构

┌─────────────────────────────────┐
│   用户界面层 (UI Layer)          │
│   - 展示数据                     │
│   - 接收用户输入                 │
└──────────────┬──────────────────┘
               │ 调用
┌──────────────▼──────────────────┐
│   应用层 (Application Layer)     │
│   - 编排用例                     │
│   - 事务边界                     │
└──────────────┬──────────────────┘
               │ 调用
┌──────────────▼──────────────────┐
│   领域层 (Domain Layer)          │
│   - 核心业务逻辑                 │
│   - 实体、值对象、领域服务       │
└──────────────┬──────────────────┘
               │ 调用
┌──────────────▼──────────────────┐
│   基础设施层 (Infrastructure)    │
│   - 数据库、外部API、消息队列    │
└─────────────────────────────────┘

代码示例:电商下单

// === 应用层 ===
@Service
public class PlaceOrderApplicationService {
    private final OrderRepository orderRepository;
    private final ProductRepository productRepository;
    private final PaymentGateway paymentGateway;

    @Transactional
    public OrderDTO placeOrder(PlaceOrderCommand command) {
        // 1. 获取领域对象
        Customer customer = customerRepository.findById(command.getCustomerId());
        List<Product> products = productRepository.findByIds(command.getProductIds());

        // 2. 执行领域逻辑
        Order order = customer.placeOrder(products, command.getShippingAddress());

        // 3. 保存
        orderRepository.save(order);

        // 4. 返回DTO(不是领域对象!)
        return OrderDTO.fromDomain(order);
    }
}

// === 领域层 ===
@Entity
public class Customer {
    public Order placeOrder(List<Product> products, Address address) {
        // 核心业务逻辑在领域层
        if (!isVIP()) {
            throw new NonVIPCustomerException();
        }
        Order order = new Order(this.id, products, address);
        return order;
    }
}

// === 基础设施层 ===
@Repository
public class OrderRepositoryImpl implements OrderRepository {
    private final EntityManager entityManager;

    public void save(Order order) {
        // 具体的数据库操作
        entityManager.persist(order);
    }
}

分层的关键
- ✅ 依赖方向:上层依赖下层,领域层不依赖任何层
- ✅ 职责清晰:应用层编排,领域层逻辑,基础设施层实现
- ✅ 可替换性:更换数据库只影响基础设施层


五、构建领域模型

7. Build the Domain Model (构建领域模型)

核心思想:从业务知识中提取领域模型

如何从零开始建模?

步骤1:事件风暴 (Event Storming)

场景:用户下单

领域事件:
- OrderPlaced (订单已创建)
- OrderPaid (订单已支付)
- OrderShipped (订单已发货)
- OrderCancelled (订单已取消)

命令:
- PlaceOrder (下单)
- PayOrder (支付)
- ShipOrder (发货)
- CancelOrder (取消)

聚合:
- Order (订单)
- Customer (客户)
- Product (商品)

步骤2:提炼模型

// 初步模型(从事件风暴得到)
public class Order {
    void place() { }  // 下单
    void pay() { }    // 支付
    void ship() { }   // 发货
    void cancel() { } // 取消
}

// 细化模型(添加业务规则)
public class Order {
    private OrderStatus status;

    public void pay(PaymentMethod method) {
        // 业务规则:只有待支付订单才能支付
        if (this.status != OrderStatus.PENDING) {
            throw new InvalidOrderStatusException();
        }
        // 业务规则:支付金额必须等于订单金额
        // ...
        this.status = OrderStatus.PAID;
    }

    public void ship() {
        // 业务规则:只有已支付订单才能发货
        if (this.status != OrderStatus.PAID) {
            throw new OrderNotPaidException();
        }
        // 业务规则:检查库存
        // ...
        this.status = OrderStatus.SHIPPED;
    }
}

步骤3:验证模型(与业务专家沟通)

开发者:订单支付后可以直接取消吗?
业务专家:不行,如果已经发货就不能取消
开发者:如果是未发货的已支付订单呢?
业务专家:可以取消,但需要退款
开发者:好的,我理解了,会更新模型

六、实践建议

何时使用 DDD?

适合场景 不适合场景
✅ 复杂的业务领域 ❌ 简单的CRUD应用
✅ 多团队协作的大型项目 ❌ 小型个人项目
✅ 业务逻辑经常变化 ❌ 技术导向的项目
✅ 需要长期维护的遗留系统重构 ❌ 快速原型验证

DDD 的代价

  • ⚠️ 学习曲线陡峭:需要理解大量概念
  • ⚠️ 前期投入大:建模需要时间
  • ⚠️ 可能过度设计:简单业务硬套DDD

建议的实践路径

  1. 从小项目开始:先在一个小模块实践DDD
  2. 与业务专家结对:建立统一语言
  3. 增量演进:不要试图一次性建模所有内容
  4. 关注业务价值:DDD的目的是解决业务问题,不是为了技术炫技

七、总结

DDD 的 8 个核心概念:

概念 作用
Business Entities 用模型表达业务概念
Model Boundaries 用边界隔离复杂度
Aggregation 保证数据一致性
Entities vs Value Objects 区分有身份和无身份的对象
Operational Modeling 用领域服务和事件封装操作
Layering 用分层组织代码结构
Build Domain Model 从业务知识中提取模型
Unified Language 让团队讲同一种语言

记住:DDD 不是银弹,它是一套让技术团队与业务专家有效沟通的方法论

真正理解 DDD,你会发现:代码不再是冰冷的机器指令,而是业务逻辑的直接表达

善忘技术夹-公众号

评论

发表评论

validate