从零构建在线点餐系统:用DDD思维拆解聚合与限界上下文
当你在餐厅扫码点餐时,是否思考过这个看似简单的操作背后隐藏着怎样的领域逻辑?本文将带你用DDD(领域驱动设计)的视角,通过构建一个真实的在线点餐系统,彻底理解聚合(Aggregate)与限界上下文(Bounded Context)这两个核心概念。
1. 需求场景分析
假设我们接到一个在线点餐系统的开发需求,核心业务流程如下:
- 顾客浏览餐厅菜单
- 将菜品加入购物车
- 提交订单并支付
- 厨房接收订单准备菜品
- 配送员取餐配送
- 顾客确认收货完成流程
关键业务规则:
- 一个订单可包含多个餐厅的菜品,但需拆分为子订单
- 订单提交后15分钟内可免费取消
- 支付超时30分钟未支付自动取消订单
- 不同菜品可能有不同的备餐时间要求
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 识别核心领域对象
通过事件风暴工作坊,我们识别出以下主要领域对象:
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
3. 划定聚合边界
聚合是DDD中最具实操价值的概念之一,它定义了数据修改的单元边界。在我们的点餐系统中,可以识别出以下聚合:
3.1 菜单聚合(Menu Aggregate)
聚合根:Menu
包含对象:
- MenuItem(实体)
- Category(值对象)
- DietaryInfo(值对象)
不变条件:
- 菜单项价格不能为负
- 同一分类下的菜单项名称必须唯一
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));
}
}
3.2 购物车聚合(Cart Aggregate)
聚合根:Cart
包含对象:
- CartItem(实体)
业务规则:
- 同一菜品多次添加应合并数量
- 购物车有效期为30分钟
3.3 订单聚合(Order Aggregate)
聚合根:Order
包含对象:
- OrderLine(实体)
- DeliveryInfo(值对象)
- PaymentInfo(值对象)
关键行为:
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));
}
}
4. 划分限界上下文
限界上下文是语义和业务能力的边界。对于我们的点餐系统,可以划分为:
4.1 菜单上下文(Menu Context)
职责:
- 管理餐厅菜单结构
- 处理菜品分类和属性
- 计算菜品可用性
与其他上下文的关系:
- 通过ID引用订单上下文中的菜品
- 发布菜品价格变更事件
4.2 订单上下文(Order Context)
核心能力:
- 处理订单生命周期
- 协调支付和配送
- 处理退单和退款
集成方式:
mermaid复制sequenceDiagram
participant OrderContext
participant PaymentContext
participant KitchenContext
OrderContext->>PaymentContext: 创建支付请求(REST)
PaymentContext-->>OrderContext: 支付结果回调
OrderContext->>KitchenContext: 发送备餐指令(Event)
4.3 厨房上下文(Kitchen Context)
领域模型:
- 接收并拆分订单
- 管理备餐进度
- 协调出餐流程
关键设计:
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()
));
}
}
5. 上下文映射与集成
不同限界上下文之间需要通过明确的协议进行通信。在我们的系统中:
| 上下文关系类型 | 实现方式 | 示例 |
|---|---|---|
| 合作关系(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);
}
}
6. 微服务拆分策略
基于限界上下文的划分,我们可以设计如下微服务架构:
code复制点餐系统架构
├── 菜单服务 (Menu Service)
│ ├── 菜品管理
│ └── 菜单发布
├── 订单服务 (Order Service)
│ ├── 购物车
│ ├── 订单处理
│ └── 支付集成
├── 厨房服务 (Kitchen Service)
│ ├── 工单管理
│ └── 备餐跟踪
└── 配送服务 (Delivery Service)
├── 骑手调度
└── 轨迹跟踪
服务间通信矩阵:
| 调用方 | 被调用方 | 协议 | 数据格式 |
|---|---|---|---|
| 订单服务 | 菜单服务 | REST | JSON |
| 订单服务 | 支付服务 | gRPC | Protobuf |
| 订单服务 | 厨房服务 | Event | Avro |
| 订单服务 | 配送服务 | GraphQL | JSON |
7. 实战中的经验教训
在真实项目落地DDD时,有几个关键点需要特别注意:
-
聚合设计陷阱:
- 避免创建"上帝聚合"(包含所有内容的超大聚合)
- 警惕聚合间的直接引用(应通过ID关联)
- 注意聚合的事务边界(一个事务只修改一个聚合)
-
上下文划分的平衡:
- 过细的划分会导致分布式事务复杂度
- 过粗的划分会失去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; -
团队协作建议:
- 建立统一的术语表(Ubiquitous Language)
- 使用上下文映射图可视化系统关系
- 定期进行领域知识分享会
8. 演进式设计
随着业务发展,我们的点餐系统可能会经历以下演进路径:
-
V1 单体架构:
- 所有上下文共用一个数据库
- 通过包(package)隔离不同上下文
-
V2 模块化单体:
code复制com.food.order ├── menu ├── ordering └── kitchen -
V3 微服务架构:
- 每个上下文独立部署
- 引入服务网格处理通信
-
V4 事件驱动架构:
- 使用事件溯源(Event Sourcing)
- 实现业务操作的可观测性
关键演进决策点:
- 当团队规模超过2个Pizza团队(约8-10人)
- 当部署频率受到单体构建时间影响
- 当不同上下文需要独立的伸缩策略时
9. 测试策略
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());
}
10. 常见问题解决方案
问题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不是抽象的理论,而是可以指导具体实践的强大工具。聚合帮助我们封装业务复杂性,限界上下文让我们理清系统边界,二者的结合能够构建出既灵活又稳定的系统架构。
