当你在餐厅扫码点餐时,是否思考过这个看似简单的操作背后隐藏着怎样的领域逻辑?本文将带你用DDD(领域驱动设计)的视角,通过构建一个真实的在线点餐系统,彻底理解聚合(Aggregate)与限界上下文(Bounded Context)这两个核心概念。
假设我们接到一个在线点餐系统的开发需求,核心业务流程如下:
关键业务规则:
通过事件风暴工作坊,我们识别出以下主要领域对象:
mermaid复制classDiagram
class MenuItem {
+String itemId
+String name
+Money price
+String description
+List~DietaryInfo~ dietaryInfos
}
class Cart {
+String cartId
+List~CartItem~ items
+addItem()
+removeItem()
}
class Order {
+String orderId
+OrderStatus status
+List~OrderLine~ lines
+submit()
+cancel()
}
class KitchenTicket {
+String ticketId
+List~TicketItem~ items
+markPrepared()
}
MenuItem "1" -- "*" CartItem
Cart "1" -- "*" CartItem
Order "1" -- "*" OrderLine
KitchenTicket "1" -- "*" TicketItem
聚合是DDD中最具实操价值的概念之一,它定义了数据修改的单元边界。在我们的点餐系统中,可以识别出以下聚合:
聚合根:Menu
包含对象:
不变条件:
java复制public class Menu {
private String restaurantId;
private List<MenuItem> items = new ArrayList<>();
public void addItem(String name, Money price, String category) {
if (price.isNegative()) {
throw new IllegalArgumentException("Price cannot be negative");
}
if (items.stream().anyMatch(i ->
i.getCategory().equals(category) && i.getName().equals(name))) {
throw new ConflictException("Duplicate item in category");
}
items.add(new MenuItem(generateId(), name, price, category));
}
}
聚合根:Cart
包含对象:
业务规则:
聚合根:Order
包含对象:
关键行为:
java复制public class Order {
public void cancel() {
if (status != OrderStatus.CREATED) {
throw new IllegalStateException("Only created orders can be cancelled");
}
if (createdAt.isBefore(LocalDateTime.now().minusMinutes(15))) {
applyCancellationFee();
}
this.status = OrderStatus.CANCELLED;
}
public void markPaid(Payment payment) {
if (status != OrderStatus.CREATED) {
throw new IllegalStateException("Only created orders can be paid");
}
this.paymentInfo = new PaymentInfo(payment);
this.status = OrderStatus.PAID;
this.domainEvents.add(new OrderPaidEvent(this));
}
}
限界上下文是语义和业务能力的边界。对于我们的点餐系统,可以划分为:
职责:
与其他上下文的关系:
核心能力:
集成方式:
mermaid复制sequenceDiagram
participant OrderContext
participant PaymentContext
participant KitchenContext
OrderContext->>PaymentContext: 创建支付请求(REST)
PaymentContext-->>OrderContext: 支付结果回调
OrderContext->>KitchenContext: 发送备餐指令(Event)
领域模型:
关键设计:
java复制public class KitchenService {
private final OrderEventPublisher publisher;
@Transactional
public void createTicket(Order order) {
KitchenTicket ticket = new KitchenTicket(order);
ticketRepository.save(ticket);
publisher.publish(new TicketCreatedEvent(
ticket.getId(),
order.getRestaurantId(),
ticket.getItems()
));
}
}
不同限界上下文之间需要通过明确的协议进行通信。在我们的系统中:
| 上下文关系类型 | 实现方式 | 示例 |
|---|---|---|
| 合作关系(Partnership) | 直接调用 | 订单→支付 |
| 客户-供应商(Customer-Supplier) | REST API | 订单→菜单 |
| 发布-订阅(Publish-Subscribe) | 领域事件 | 订单→厨房 |
| 防腐层(ACL) | 适配器模式 | 第三方配送系统集成 |
支付上下文集成示例:
java复制// 领域层定义接口
public interface PaymentService {
PaymentResult process(PaymentCommand command);
}
// 基础设施层实现
public class RestPaymentService implements PaymentService {
private final PaymentClient client;
@Override
public PaymentResult process(PaymentCommand command) {
PaymentRequest request = assembleRequest(command);
PaymentResponse response = client.createPayment(request);
return assembleResult(response);
}
}
基于限界上下文的划分,我们可以设计如下微服务架构:
code复制点餐系统架构
├── 菜单服务 (Menu Service)
│ ├── 菜品管理
│ └── 菜单发布
├── 订单服务 (Order Service)
│ ├── 购物车
│ ├── 订单处理
│ └── 支付集成
├── 厨房服务 (Kitchen Service)
│ ├── 工单管理
│ └── 备餐跟踪
└── 配送服务 (Delivery Service)
├── 骑手调度
└── 轨迹跟踪
服务间通信矩阵:
| 调用方 | 被调用方 | 协议 | 数据格式 |
|---|---|---|---|
| 订单服务 | 菜单服务 | REST | JSON |
| 订单服务 | 支付服务 | gRPC | Protobuf |
| 订单服务 | 厨房服务 | Event | Avro |
| 订单服务 | 配送服务 | GraphQL | JSON |
在真实项目落地DDD时,有几个关键点需要特别注意:
聚合设计陷阱:
上下文划分的平衡:
性能考量:
sql复制/* 反模式:跨聚合的复杂查询 */
SELECT o.*, i.*
FROM orders o
JOIN order_items i ON o.id = i.order_id
JOIN menu_items m ON i.item_id = m.id
WHERE m.category = '主食';
/* 正确做法:使用CQRS分离读写模型 */
CREATE VIEW order_summary AS
SELECT o.id, o.status,
JSON_ARRAYAGG(JSON_OBJECT(
'name', m.name,
'price', i.price
)) AS items
FROM orders o
JOIN order_items i ON o.id = i.order_id
JOIN menu_items m ON i.item_id = m.id
GROUP BY o.id;
团队协作建议:
随着业务发展,我们的点餐系统可能会经历以下演进路径:
V1 单体架构:
V2 模块化单体:
code复制com.food.order
├── menu
├── ordering
└── kitchen
V3 微服务架构:
V4 事件驱动架构:
关键演进决策点:
DDD系统需要特别关注测试金字塔的构建:
code复制测试金字塔
├── 单元测试(聚合内行为)
├── 集成测试(上下文内组件)
├── 契约测试(上下文间接口)
└── 端到端测试(完整业务流程)
聚合测试示例:
java复制@Test
void should_reject_negative_prices() {
Menu menu = new Menu("restaurant1");
assertThrows(IllegalArgumentException.class, () -> {
menu.addItem("Test Item", Money.of(-1, "CNY"), "测试");
});
}
@Test
void should_allow_valid_order_cancellation() {
Order order = new Order(testCustomer, testItems);
order.submit();
order.cancel();
assertEquals(OrderStatus.CANCELLED, order.getStatus());
}
问题1:如何处理跨聚合的业务规则?
解决方案:使用领域服务协调多个聚合
java复制public class OrderService {
public void applyDiscount(Order order, Discount discount) {
order.applyDiscount(discount); // 修改订单聚合
inventory.adjustForDiscount(discount); // 修改库存聚合
}
}
问题2:如何保证最终一致性?
解决方案:使用Saga模式
mermaid复制sequenceDiagram
participant O as OrderService
participant P as PaymentService
participant K as KitchenService
O->>P: 创建支付
P-->>O: 支付成功
O->>K: 创建工单
alt 工单创建失败
O->>P: 发起退款
end
问题3:如何应对高频查询?
解决方案:实现CQRS查询端
java复制public class OrderQueryService {
public OrderSummary getOrderSummary(String orderId) {
return queryDatabase(
"SELECT * FROM order_summary WHERE id = ?",
orderId
);
}
}
通过这个在线点餐系统的完整案例,我们可以看到DDD不是抽象的理论,而是可以指导具体实践的强大工具。聚合帮助我们封装业务复杂性,限界上下文让我们理清系统边界,二者的结合能够构建出既灵活又稳定的系统架构。