# DDD 概念

作者:Ethan.Yang
博客:https://blog.ethanyang.cn (opens new window)


领域驱动设计(Domain-Driven Design, DDD)是由Eric Evans在其书《领域驱动设计》中提出的一种软件设计理念,旨在通过对业务领域的深入理解来构建复杂的业务系统。DDD强调将技术架构与业务需求紧密结合,从而有效地应对复杂性。

# 领域(Domain)

领域是指系统所涉及的业务领域或问题空间。它描述了系统所服务的业务的各种事物和规则。理解领域是DDD的核心,因为只有深入了解业务领域,才能设计出符合实际需求的系统。

# 领域的特点:

  • 业务问题:领域涉及的通常是实际的业务问题,比如金融、医疗、零售等。
  • 领域专家:领域专家是对领域有深刻理解的人,他们的经验和知识对于设计系统至关重要。
// 商品领域
@Data
public class Product {
    private String productId;
    private String name;
    private double price;  // 商品价格
    
    public String getProductId() {
        return productId;
    }

    public String getName() {
        return name;
    }

    public double getPrice() {
        return price;
    }
}

// 订单项领域
@Data
public class OrderItem {
    private Product product;  // 商品
    private int quantity;  // 商品数量
    
    public double getTotalPrice() {
        return product.getPrice() * quantity;
    }

    public Product getProduct() {
        return product;
    }

    public int getQuantity() {
        return quantity;
    }
}


// 订单领域
public class Order {
    private String orderId;
    private List<OrderItem> items;  // 订单中的商品项
    private String status;  // 订单状态
    private double totalPrice;  // 订单总价
    
    public void addItem(OrderItem item) {
        items.add(item);
        this.totalPrice = calculateTotalPrice();
    }
    
    private double calculateTotalPrice() {
        return items.stream().mapToDouble(OrderItem::getTotalPrice).sum();
    }
    
    public String getStatus() {
        return status;
    }
    
    public void pay() {
        if ("未支付".equals(status)) {
            status = "已支付";  // 支付操作
        } else {
            throw new IllegalStateException("订单已支付或无效");
        }
    }

    public double getTotalPrice() {
        return totalPrice;
    }
}


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74

# 子域(Subdomain)

一个大型的系统往往会涉及多个领域,子域是一个特定的领域的子集。每个子域都有自己的业务逻辑和规则,可以独立地开发和维护。

# 子域的分类:

  • 核心子域(Core Subdomain):对企业最为关键的领域,往往是系统的核心部分。
  • 支持子域(Supporting Subdomain):为核心子域提供支持的领域。
  • 通用子域(Generic Subdomain):适用于多个行业或企业的通用领域,往往可以用现成的解决方案。

# 模型(Model)

模型是对业务领域的简化和抽象,它帮助开发者理解和描述领域的概念、业务流程和规则。在DDD中,模型不仅仅是技术上的设计图,还是和领域专家讨论业务逻辑的工具。

# 模型的特点:

  • Ubiquitous Language(通用语言):模型中的所有术语和概念都应当与领域专家的语言一致,从而促进跨学科的沟通。
  • 表达业务:模型的目的是表达业务领域中的重要概念和行为。

# 聚合(Aggregate)

聚合是一个由多个实体和值对象组成的集合,它在一个业务场景中表现为一个单元。聚合的设计目的是确保领域模型的一致性和完整性。

# 聚合的特点:

  • 根实体(Aggregate Root):聚合中的一个实体,作为聚合的入口点。所有外部访问聚合的数据和操作都必须通过根实体。
  • 一致性边界:聚合内的对象必须保持一致性,聚合外的对象可以独立地发生变化。
@Data
public class Order {
    private String orderId;
    private List<OrderItem> items;
    private String status;
    private double totalPrice;

    public void addItem(OrderItem item) {
        items.add(item);
        this.totalPrice = calculateTotalPrice();
    }

    private double calculateTotalPrice() {
        return items.stream().mapToDouble(OrderItem::getTotalPrice).sum();
    }

    public void pay() {
        if ("未支付".equals(status)) {
            status = "已支付";
        } else {
            throw new IllegalStateException("订单已支付或无效");
        }
    }
    
    public List<OrderItem> getItems() {
        return items;
    }

    public double getTotalPrice() {
        return totalPrice;
    }

    public String getStatus() {
        return status;
    }
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37

Order 聚合根负责管理订单项(OrderItem)和订单的状态。

# 实体(Entity)

实体是指在业务中有标识符且生命周期内可能发生变化的对象。实体通过唯一标识符来区分其他实体。

# 实体的特点:

  • 标识符:实体必须有唯一标识符(ID),即使实体的属性发生变化,它依然保持唯一性。
  • 生命周期:实体通常有明确的生命周期,可能会被创建、修改或删除。

# 值对象(Value Object)

值对象是没有标识符、只通过属性定义的对象。在业务中,它通常用来表示某个不可变的概念。

# 值对象的特点:

  • 不可变性:一旦创建,值对象的状态就不能改变。任何变化都会生成一个新的值对象。
  • 无标识符:值对象通过其属性来唯一标识,而不是通过ID。
@Data
@Getter
public class Money {
    private double amount;
    private String currency;

    // 不可变的对象
    public Money add(Money other) {
        if (!this.currency.equals(other.currency)) {
            throw new IllegalArgumentException("Currency mismatch");
        }
        return new Money(this.amount + other.amount, this.currency);
    }
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

Money 是一个值对象,它表示金额的概念,可以用于订单价格的计算。

# 领域服务(Domain Service)

领域服务是指执行一些不属于任何一个实体或值对象的操作的服务。领域服务通常用于处理复杂的业务逻辑,或者提供跨多个聚合的操作。

# 领域服务的特点:

  • 无状态:领域服务通常没有自己的状态,它们通过输入和输出与领域模型进行交互。
  • 业务逻辑集中:领域服务负责封装跨多个聚合的复杂业务逻辑。
// 订单服务
public class OrderService {
    private OrderRepository orderRepository;

    public OrderService(OrderRepository orderRepository) {
        this.orderRepository = orderRepository;
    }

    public Order createOrder(String orderId, List<OrderItem> items) {
        Order order = new Order(orderId);
        items.forEach(order::addItem);
        orderRepository.save(order);
        return order;
    }

    public void payOrder(String orderId) {
        Order order = orderRepository.findById(orderId);
        order.pay();
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

OrderService 提供了业务逻辑,例如创建订单和支付订单。

# 仓储(Repository)

仓储是用于持久化和检索领域模型(如聚合根)的机制。它将领域模型与数据存储之间的映射抽象化,提供一个清晰的接口来访问领域对象。

# 仓储的特点:

  • 聚焦聚合根:仓储通常操作聚合根,其他对象通常不直接存储或访问。
  • 持久化操作:仓储通过持久化技术来存储和检索领域对象。
// 订单仓储
public interface OrderRepository {
    void save(Order order);
    Order findById(String orderId);
}

public class InMemoryOrderRepository implements OrderRepository {
    private Map<String, Order> orderDatabase = new HashMap<>();

    @Override
    public void save(Order order) {
        orderDatabase.put(order.getOrderId(), order);
    }

    @Override
    public Order findById(String orderId) {
        return orderDatabase.get(orderId);
    }
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

InMemoryOrderRepository 实现了 OrderRepository 接口,通过内存中的 Map 来存储和查找订单。

# 领域事件(Domain Event)

领域事件是指在领域模型中发生的、对其他部分有意义的事件。它通常表示业务状态的改变。

# 领域事件的特点:

  • 业务事件:领域事件描述了业务发生的某个重要事件,比如订单创建、支付完成等。
  • 异步处理:领域事件可以被其他系统或组件异步处理,通常与事件驱动架构结合使用。
// 订单支付事件
public class OrderPaidEvent {
    private String orderId;

    public OrderPaidEvent(String orderId) {
        this.orderId = orderId;
    }

    public String getOrderId() {
        return orderId;
    }
}

1
2
3
4
5
6
7
8
9
10
11
12
13

OrderPaidEvent 表示一个订单支付成功的事件,可以通过事件发布机制进行通知。

# 防腐层(Anticorruption Layer)

防腐层是一种模式,用来保护领域模型免受外部系统(如遗留系统或第三方系统)影响。通过防腐层,外部系统的变化不会直接影响到系统内部的核心领域模型。

# 防腐层的特点:

  • 接口适配:防腐层将外部系统的接口转换成领域模型能够理解的格式。
  • 数据转换:防腐层负责将外部系统的数据转换成领域模型所需的格式,避免外部系统对领域模型的直接影响。
// 对外提供支付功能
public class ExternalPaymentSystem {
    public boolean processPayment(String orderId, double amount) {
        // 模拟支付处理
        System.out.println("Processing payment for Order ID: " + orderId);
        return true;
    }
}

public class PaymentAdapter {
    private ExternalPaymentSystem externalPaymentSystem;

    public PaymentAdapter(ExternalPaymentSystem externalPaymentSystem) {
        this.externalPaymentSystem = externalPaymentSystem;
    }

    public boolean makePayment(String orderId, double amount) {
        return externalPaymentSystem.processPayment(orderId, amount);
    }
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

PaymentAdapter 是防腐层,它将外部支付系统的接口转换为我们系统可以理解和使用的方式。

# 总结

领域:订单、商品、支付等。

聚合:订单是聚合根,管理商品项。

实体:订单和订单项是实体,通过唯一标识符来区分。

值对象:金额(Money)是不可变的值对象。

领域服务:如订单服务(OrderService),封装业务逻辑。

仓储:如订单仓储(OrderRepository),用于持久化。

领域事件:如订单支付事件。

防腐层:用于隔离外部系统。

# DDD 设计原则

# 领域驱动设计的核心是聚焦业务

DDD 的设计原则之一就是要聚焦业务,理解领域和业务需求。在系统设计时,要始终将关注点放在业务上,确保系统能够表达和解决实际的业务问题。

# 示例:业务驱动设计

假设我们正在设计一个电商系统中的订单服务。在这个服务中,我们的核心关注点就是“如何管理订单”。我们通过聚焦业务,创建了订单(Order)这一核心聚合对象。订单聚合包含多个订单项(OrderItem),每个订单项包含一个商品(Product)。这些领域对象和业务行为共同帮助我们实现电商平台的订单管理功能。

@Data
public class Order {
    private String orderId;
    private List<OrderItem> items;
    private double totalPrice;

    public void addItem(OrderItem item) {
        items.add(item);
        this.totalPrice = calculateTotalPrice();
    }

    private double calculateTotalPrice() {
        return items.stream().mapToDouble(OrderItem::getTotalPrice).sum();
    }

    public double getTotalPrice() {
        return totalPrice;
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

在这个代码中,Order 这个聚合根关注的就是如何管理商品项和计算订单总价。业务需求是我们聚焦的核心。

# 聚合设计原则:确保一致性和封装

聚合是 DDD 中的重要概念,它代表了一组领域对象,它们必须通过聚合根来访问。聚合的设计要确保一致性,即在同一个聚合内进行的操作应该是原子性的。聚合的封装性也非常重要,聚合外的代码不能直接修改聚合内部的状态。

# 示例:订单聚合的设计

我们继续使用订单例子来解释聚合设计。Order 是一个聚合根,它管理 OrderItem 的集合。外部无法直接修改 OrderItem,只能通过 Order 聚合根提供的接口进行操作。这保证了聚合内的一致性和封装性。

@Data
public class Order {
    private String orderId;
    private List<OrderItem> items;
    private String status;

    // 聚合根方法,不能直接操作 OrderItem
    public void addItem(OrderItem item) {
        items.add(item);
    }

    public void pay() {
        if ("未支付".equals(status)) {
            status = "已支付";
        } else {
            throw new IllegalStateException("订单已支付");
        }
    }

    public List<OrderItem> getItems() {
        return items;
    }

    public String getStatus() {
        return status;
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27

在这里,Order 是聚合根,它封装了所有与订单相关的操作。外部不能直接操作 OrderItem,只能通过 Order 来添加商品项,支付订单等。这确保了聚合内的一致性。

# 限界上下文(Bounded Context)原则

限界上下文是 DDD 中的关键概念,它定义了系统中不同部分的边界和业务模型。每个限界上下文都有自己独立的业务模型,这些模型之间的交互通过明确定义的接口进行。限界上下文原则强调不同模块之间的隔离和解耦。

# 示例:限界上下文

在电商系统中,订单模块、支付模块和库存模块可能是不同的限界上下文。每个限界上下文有自己独立的业务逻辑,且它们之间的交互要通过接口或事件来完成。例如,支付模块不直接操作订单模块的数据,而是通过发送支付成功事件或调用订单模块提供的接口来更新订单状态。

// 支付模块中的支付成功事件
@Data
public class PaymentSucceededEvent {
    private String orderId;
    
    public PaymentSucceededEvent(String orderId) {
        this.orderId = orderId;
    }

    public String getOrderId() {
        return orderId;
    }
}

// 订单模块处理支付成功事件
public class OrderService {
    public void handlePaymentSucceeded(PaymentSucceededEvent event) {
        Order order = orderRepository.findById(event.getOrderId());
        order.pay();
        orderRepository.save(order);
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

这里,PaymentSucceededEvent 表示支付成功的事件,订单模块通过订阅这个事件来更新订单状态。不同的限界上下文通过事件解耦,确保了系统的模块化和高内聚低耦合。

# 领域事件(Domain Events)设计原则

领域事件用于表示一个重要的业务事件,它在业务领域内传播,可能会触发其他业务操作。领域事件设计原则强调了事件驱动模型的使用,它帮助我们解耦不同模块之间的交互,使得系统更加灵活和扩展性更强。

# 示例:领域事件

继续用订单系统来演示领域事件。假设在支付成功后,我们需要通知库存系统减少库存数量,这时我们可以使用领域事件来完成。

// 订单支付成功领域事件
@Data
public class OrderPaidEvent {
    private String orderId;

    public OrderPaidEvent(String orderId) {
        this.orderId = orderId;
    }

    public String getOrderId() {
        return orderId;
    }
}

// 领域事件处理器
public class InventoryService {
    public void handleOrderPaid(OrderPaidEvent event) {
        // 在支付成功后,更新库存
        System.out.println("Order " + event.getOrderId() + " paid. Reducing inventory.");
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

在这个例子中,OrderPaidEvent 表示订单支付成功的事件,InventoryService 处理这个事件来减少库存。通过领域事件的设计,支付和库存操作解耦,系统更加灵活。

# 防腐层(Anticorruption Layer)原则

防腐层用于保护我们领域模型不受外部系统变化的影响。它充当了领域模型和外部系统之间的隔离层,防止外部系统的变化直接影响我们的领域模型。

# 示例:防腐层

假设我们有一个外部支付系统,我们可以使用防腐层来适配外部支付系统的接口,使其与我们的领域模型兼容。

// 外部支付系统
@Data
public class ExternalPaymentSystem {
    public boolean processPayment(String orderId, double amount) {
        // 模拟支付处理
        System.out.println("Processing payment for Order ID: " + orderId);
        return true;
    }
}

// 防腐层,适配外部支付系统
public class PaymentAdapter {
    private ExternalPaymentSystem externalPaymentSystem;

    public PaymentAdapter(ExternalPaymentSystem externalPaymentSystem) {
        this.externalPaymentSystem = externalPaymentSystem;
    }

    public boolean makePayment(String orderId, double amount) {
        return externalPaymentSystem.processPayment(orderId, amount);
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

在这个例子中,PaymentAdapter 是防腐层,它将外部支付系统的接口适配到我们系统可以理解和使用的方式。这保证了外部系统的变化不会直接影响我们的领域模型。

# 总结

DDD 设计原则为我们提供了如何组织和管理复杂业务的框架,确保系统在面对变化时仍能保持灵活性和扩展性。通过以下原则,我们能够构建出更具高内聚低耦合的系统:

  • 聚焦业务:始终关注业务需求,设计符合业务的模型。
  • 聚合设计原则:确保聚合内的一致性和封装性。
  • 限界上下文:明确系统的不同模块和边界,避免跨模块的业务逻辑混乱。
  • 领域事件:使用事件驱动模型解耦系统模块,提高扩展性。
  • 防腐层:保护领域模型不受外部系统变化的影响。

这些设计原则将帮助我们在开发过程中保持业务与技术的高度一致,让系统随着业务需求的变化能够灵活扩展。