你是世界顶尖架构师,已经做到了返璞归真的境界,你擅长用通俗的语言,将架构的本质,讲给困惑的小白,使其醍醐灌顶。
什么是DDD,DDD的起源是什么,它的产生是必然的吗?
为什么要有 application、domain、infrastructure、interfaces?这样设计一定是好的吗?
或者你可以给我从起源讲起,比如最开始的混乱,到后来的 mvc,再到最后的 ddd,以及未来还有什么
talk is more,你可以用 java 代码,验证你的结论
最后,因为大模型输出可能会有限制,让我得到的答案不完整,所以,我希望,你不要考虑输出限制,如果最后平台输出不了,我会输入继续,让你继续输出,所以,请不要担心,我希望,哪怕答案冗余,也请不要遗漏任何关键信息
从混沌到DDD:一个架构师的返璞归真之旅
第一章:远古时代------混沌的代码
让我们穿越回2000年代初期。那时候的程序员写代码是这样的:
java
// 这是一个"真实存在过"的代码风格
// 文件名:OrderServlet.java ------ 一个Servlet处理所有事情
public class OrderServlet extends HttpServlet {
protected void doPost(HttpServletRequest request, HttpServletResponse response) {
// 1. 从请求里拿数据
String userId = request.getParameter("userId");
String productId = request.getParameter("productId");
int quantity = Integer.parseInt(request.getParameter("quantity"));
// 2. 直接连数据库(JDBC硬编码)
Connection conn = null;
try {
conn = DriverManager.getConnection(
"jdbc:mysql://localhost:3306/shop", "root", "123456"
);
// 3. 查库存(SQL直接写在这里)
PreparedStatement ps = conn.prepareStatement(
"SELECT stock FROM product WHERE id = ?"
);
ps.setString(1, productId);
ResultSet rs = ps.executeQuery();
int stock = 0;
if (rs.next()) {
stock = rs.getInt("stock");
}
// 4. 业务逻辑也写在这里
if (stock < quantity) {
response.getWriter().write("库存不足");
return;
}
// 5. 计算价格(还是在这里)
PreparedStatement ps2 = conn.prepareStatement(
"SELECT price FROM product WHERE id = ?"
);
ps2.setString(1, productId);
ResultSet rs2 = ps2.executeQuery();
double price = 0;
if (rs2.next()) {
price = rs2.getDouble("price");
}
double totalPrice = price * quantity;
// 6. 插入订单
PreparedStatement ps3 = conn.prepareStatement(
"INSERT INTO orders(user_id, product_id, quantity, total_price, status) VALUES(?,?,?,?,?)"
);
ps3.setString(1, userId);
ps3.setString(2, productId);
ps3.setInt(3, quantity);
ps3.setDouble(4, totalPrice);
ps3.setString(5, "PENDING");
ps3.executeUpdate();
// 7. 扣库存
PreparedStatement ps4 = conn.prepareStatement(
"UPDATE product SET stock = stock - ? WHERE id = ?"
);
ps4.setInt(1, quantity);
ps4.setString(2, productId);
ps4.executeUpdate();
// 8. 发邮件通知(也在这里!)
Properties props = new Properties();
props.put("mail.smtp.host", "smtp.example.com");
// ... 几十行邮件代码
response.getWriter().write("下单成功");
} catch (Exception e) {
// 吞掉异常,什么都不做(这很常见)
e.printStackTrace();
} finally {
// 经常忘记关闭连接
}
}
}
这段代码的问题是什么?
你可能会说:太乱了。但我要问你:乱在哪里?
本质问题只有一个:关注点全部混在一起了。
- HTTP协议的关注点(怎么接收请求)
- 业务逻辑的关注点(能不能下单)
- 数据持久化的关注点(怎么存数据库)
- 通知的关注点(怎么发邮件)
这四件完全不同性质的事,被塞进了同一个方法。
后果是:
- 改个数据库字段名,你要翻遍所有Servlet
- 想测试"库存不足"的逻辑,你必须启动整个Web容器
- 换个发邮件的服务商,你不知道要改哪几个地方
- 新来的同事看这段代码,脑子直接宕机
第二章:MVC的出现------人类第一次觉醒
程序员们意识到了:我们需要分层。
于是MVC诞生了(其实更早有三层架构,但道理一样)。
Controller(控制器)------ 负责接收请求,返回响应
Service(服务层) ------ 负责业务逻辑
DAO/Repository ------ 负责数据库操作
Model/Entity ------ 负责承载数据
代码变成了这样:
java
// ===== Controller层 =====
@RestController
@RequestMapping("/orders")
public class OrderController {
@Autowired
private OrderService orderService;
@PostMapping
public ResponseEntity<String> createOrder(@RequestBody CreateOrderRequest request) {
orderService.createOrder(
request.getUserId(),
request.getProductId(),
request.getQuantity()
);
return ResponseEntity.ok("下单成功");
}
}
// ===== Service层 =====
@Service
public class OrderService {
@Autowired
private OrderDao orderDao;
@Autowired
private ProductDao productDao;
@Autowired
private EmailService emailService;
public void createOrder(String userId, String productId, int quantity) {
// 查库存
Product product = productDao.findById(productId);
if (product.getStock() < quantity) {
throw new RuntimeException("库存不足");
}
// 创建订单
Order order = new Order();
order.setUserId(userId);
order.setProductId(productId);
order.setQuantity(quantity);
order.setTotalPrice(product.getPrice() * quantity);
order.setStatus("PENDING");
orderDao.save(order);
// 扣库存
product.setStock(product.getStock() - quantity);
productDao.update(product);
// 发邮件
emailService.sendOrderConfirmation(userId, order);
}
}
// ===== DAO层 =====
@Repository
public class OrderDao {
@Autowired
private JdbcTemplate jdbcTemplate;
public void save(Order order) {
jdbcTemplate.update(
"INSERT INTO orders(user_id, product_id, quantity, total_price, status) VALUES(?,?,?,?,?)",
order.getUserId(), order.getProductId(),
order.getQuantity(), order.getTotalPrice(), order.getStatus()
);
}
}
// ===== Model =====
public class Order {
private String id;
private String userId;
private String productId;
private int quantity;
private double totalPrice;
private String status;
// getter/setter...
}
MVC解决了什么问题?
它解决了技术职责的分离:接收请求的归接收请求,操作数据库的归操作数据库。
这是一次巨大的进步。团队可以分工了:前端同学改Controller,DBA同学改DAO,业务同学改Service。
但是,MVC带来了一个隐藏的大坑。
第三章:MVC的致命缺陷------贫血模型
注意看上面那个Order类:
java
public class Order {
private String id;
private String userId;
private String productId;
private int quantity;
private double totalPrice;
private String status;
// 只有getter/setter,没有任何行为!
}
这个Order是什么?它只是一个数据容器 ,一个哑巴对象。它只有数据,没有行为。
所有的业务逻辑都堆在OrderService里了。
随着业务增长,你的OrderService会变成这样:
java
@Service
public class OrderService {
// 几千行代码
public void createOrder(...) { /* 100行 */ }
public void cancelOrder(...) { /* 80行 */ }
public void payOrder(...) { /* 120行 */ }
public void refundOrder(...) { /* 150行 */ }
public void shipOrder(...) { /* 90行 */ }
public void completeOrder(...) { /* 60行 */ }
public void calculateDiscount(...) { /* 200行,各种促销逻辑 */ }
public void checkInventory(...) { /* 70行 */ }
// ...还有几十个方法
// 这个类已经3000行了,没人敢动它
}
这就是著名的"Service爆炸"现象。
Martin Fowler给这种现象起了个名字:贫血领域模型(Anemic Domain Model),并称之为反模式。
为什么是反模式?
因为面向对象的本质是:数据和行为在一起。
一个Order对象,它自己应该知道:
- 我能不能被取消(不是任何状态都能取消)
- 我的总价怎么算(不应该让外部来算)
- 我能不能被支付(已取消的订单不能支付)
但在贫血模型里,Order什么都不知道,它只是个哑巴,所有智慧都在Service里。
第四章:业务复杂性的爆炸------DDD出现的必然性
2003年,Eric Evans出版了《Domain-Driven Design》(领域驱动设计)。
为什么是必然的?
因为软件行业遇到了一个根本性的困境:
技术越来越简单,业务越来越复杂。
框架解决了技术问题------Spring帮你管Bean,Hibernate帮你操作数据库,Tomcat帮你处理HTTP。
但没有任何框架能帮你解决:
- "一个订单在什么状态下能退款?"
- "VIP用户和普通用户的优惠规则不同,怎么建模?"
- "当一笔支付失败时,库存是否应该自动释放?"
这些业务规则,才是真正让软件变复杂的东西。
Evans的核心洞见只有一句话:
软件的核心复杂性在于业务领域本身,而不是技术。
所以,解决之道是:让代码的结构,和业务的结构保持一致。
第五章:DDD是什么------用大白话解释
DDD的核心概念,我用生活中的例子来解释:
5.1 领域(Domain)
领域就是你要解决的那个问题的范围。
比如,你在做一个电商系统,"电商"就是你的领域。在这个大领域里,有:
- 订单子域
- 库存子域
- 支付子域
- 用户子域
- 营销子域
5.2 限界上下文(Bounded Context)------这是最重要的概念
想象一个词:"用户"
在订单上下文里,用户是:下单的人,我关心他的收货地址、历史订单。
在营销上下文里,用户是:被推销的对象,我关心他的购买偏好、积分等级。
在客服上下文里,用户是:投诉的人,我关心他的问题历史、情绪。
同一个词,在不同上下文里,含义完全不同。
这就是为什么我们需要限界上下文------在一个明确的边界内,词汇有唯一、精确的含义。
java
// 订单上下文里的"用户"
package com.example.order.domain;
public class OrderCustomer { // 注意:不叫User,叫OrderCustomer
private CustomerId id;
private ShippingAddress defaultAddress;
private List<Order> recentOrders;
// 只包含订单上下文关心的信息
}
// 营销上下文里的"用户"
package com.example.marketing.domain;
public class MarketingMember { // 叫MarketingMember
private MemberId id;
private MemberLevel level; // 金牌、银牌、铜牌
private BigDecimal totalPoints;
private List<Coupon> availableCoupons;
// 只包含营销上下文关心的信息
}
5.3 聚合根(Aggregate Root)------DDD最精华的设计
聚合根是一组相关对象的根节点,外界只能通过它来操作这组对象。
用订单举例:
java
// 订单聚合根
public class Order {
private OrderId id;
private OrderStatus status;
private List<OrderItem> items; // 订单项,只能通过Order来管理
private Money totalAmount;
private CustomerId customerId;
// ===== 工厂方法,创建订单 =====
public static Order create(CustomerId customerId, List<OrderItemCommand> itemCommands) {
Order order = new Order();
order.id = OrderId.generate();
order.customerId = customerId;
order.status = OrderStatus.CREATED;
order.items = new ArrayList<>();
for (OrderItemCommand cmd : itemCommands) {
order.items.add(new OrderItem(cmd.getProductId(), cmd.getQuantity(), cmd.getPrice()));
}
order.calculateTotal(); // 自己计算总价,不需要外部帮忙
// 发布领域事件
order.addDomainEvent(new OrderCreatedEvent(order.id, order.customerId));
return order;
}
// ===== 业务行为都在这里,不在Service里 =====
public void cancel(String reason) {
// 业务规则:只有CREATED和PAID状态可以取消
if (this.status != OrderStatus.CREATED && this.status != OrderStatus.PAID) {
throw new DomainException("订单状态为[" + status + "],无法取消");
}
this.status = OrderStatus.CANCELLED;
this.addDomainEvent(new OrderCancelledEvent(this.id, reason));
}
public void pay(PaymentId paymentId) {
if (this.status != OrderStatus.CREATED) {
throw new DomainException("只有待支付的订单才能支付");
}
this.status = OrderStatus.PAID;
this.addDomainEvent(new OrderPaidEvent(this.id, paymentId));
}
public void ship(TrackingNumber trackingNumber) {
if (this.status != OrderStatus.PAID) {
throw new DomainException("只有已支付的订单才能发货");
}
this.status = OrderStatus.SHIPPED;
this.addDomainEvent(new OrderShippedEvent(this.id, trackingNumber));
}
// ===== 私有方法,外部无法直接调用 =====
private void calculateTotal() {
this.totalAmount = items.stream()
.map(OrderItem::getSubtotal)
.reduce(Money.ZERO, Money::add);
}
// 注意:没有setter!外部不能随便修改状态
// 只能通过业务方法(cancel/pay/ship)来改变状态
}
// 订单项,是Order聚合的内部成员,外部不能直接操作它
public class OrderItem {
private ProductId productId;
private int quantity;
private Money unitPrice;
// 包级私有的构造器,只有Order能创建它
OrderItem(ProductId productId, int quantity, Money unitPrice) {
if (quantity <= 0) throw new DomainException("数量必须大于0");
this.productId = productId;
this.quantity = quantity;
this.unitPrice = unitPrice;
}
public Money getSubtotal() {
return unitPrice.multiply(quantity);
}
// 没有setter,不可变
}
看到区别了吗?
在MVC的贫血模型里:
Order只有getter/setter,是个哑巴OrderService.cancelOrder()里写着取消的规则
在DDD的充血模型里:
Order自己知道什么时候能取消:order.cancel(reason)Order自己保护自己的状态,外部无法随意修改- 业务规则就住在业务对象里
第六章:为什么是这四层------application、domain、infrastructure、interfaces
现在我们来回答你最核心的问题。
先看整体结构:
interfaces/ ← 门面层(对外的接口)
└── rest/ HTTP接口
└── grpc/ RPC接口
└── consumer/ 消息消费者
application/ ← 应用层(编排者)
└── service/ 应用服务
└── command/ 命令对象
└── query/ 查询对象
domain/ ← 领域层(大脑,最重要)
└── model/ 实体、值对象、聚合根
└── service/ 领域服务
└── repository/ 仓储接口(只是接口!)
└── event/ 领域事件
infrastructure/ ← 基础设施层(手和脚)
└── persistence/ 数据库实现
└── messaging/ 消息队列实现
└── external/ 外部服务调用
为什么这么分?一个比喻就够了。
把一家餐厅想象成你的系统:
| 层 | 餐厅对应 | 职责 |
|---|---|---|
| interfaces | 前台/收银台 | 接待顾客,翻译顾客的话 |
| application | 大堂经理 | 协调各部门,不做具体工作 |
| domain | 厨师/厨房 | 真正的核心业务,怎么做菜 |
| infrastructure | 采购/洗碗工/设备 | 支撑厨房工作的一切 |
最关键的依赖规则:
外层依赖内层,内层永远不依赖外层。
domain层不知道database的存在,不知道HTTP的存在,不知道消息队列的存在。
interfaces → application → domain ← infrastructure
↑
这是核心,所有人服务于它
让我用代码来证明这个依赖关系:
java
// ===== domain层:定义Repository接口,但不实现它 =====
// 注意:这个接口在domain包里,它只关心业务语义
package com.example.order.domain.repository;
public interface OrderRepository {
void save(Order order);
Optional<Order> findById(OrderId orderId);
List<Order> findByCustomerId(CustomerId customerId);
// 没有任何数据库相关的代码!
}
// ===== infrastructure层:实现这个接口 =====
// 注意:这个实现在infrastructure包里,它知道数据库
package com.example.order.infrastructure.persistence;
@Repository
public class OrderRepositoryImpl implements OrderRepository {
@Autowired
private OrderJpaRepository jpaRepository; // JPA的东西
@Autowired
private OrderMapper mapper; // 把DO转成Domain对象
@Override
public void save(Order order) {
OrderDO orderDO = mapper.toDO(order);
jpaRepository.save(orderDO);
}
@Override
public Optional<Order> findById(OrderId orderId) {
return jpaRepository.findById(orderId.getValue())
.map(mapper::toDomain);
}
}
// ===== domain层:永远不知道上面这个Impl的存在 =====
// domain层只依赖自己定义的接口
这就是依赖倒置原则(DIP)的体现:
高层(domain)不依赖低层(infrastructure),而是定义接口,低层来实现。
这样,如果你要从MySQL换成MongoDB,你只需要换掉OrderRepositoryImpl,domain层的代码一行不动。
第七章:四层各司其职的完整代码
让我用一个完整的"下订单"流程,串联起所有四层:
java
// =============================================
// interfaces层:接收HTTP请求
// =============================================
package com.example.order.interfaces.rest;
@RestController
@RequestMapping("/api/v1/orders")
public class OrderController {
private final CreateOrderCommandHandler commandHandler;
@PostMapping
public ResponseEntity<OrderResponse> createOrder(
@RequestBody @Valid CreateOrderRequest request,
@AuthenticationPrincipal UserPrincipal principal) {
// interfaces层的职责:
// 1. 接收HTTP参数
// 2. 转换成应用层命令
// 3. 调用应用层
// 4. 把结果转成HTTP响应
CreateOrderCommand command = CreateOrderCommand.builder()
.customerId(principal.getUserId())
.items(request.getItems().stream()
.map(i -> new OrderItemCommand(i.getProductId(), i.getQuantity()))
.collect(toList()))
.build();
OrderId orderId = commandHandler.handle(command);
return ResponseEntity
.created(URI.create("/api/v1/orders/" + orderId.getValue()))
.body(new OrderResponse(orderId.getValue()));
}
}
// =============================================
// application层:编排业务流程
// =============================================
package com.example.order.application.service;
@Service
@Transactional
public class CreateOrderCommandHandler {
private final OrderRepository orderRepository; // domain层接口
private final ProductService productService; // 另一个domain服务
private final DomainEventPublisher eventPublisher; // 基础设施接口
public OrderId handle(CreateOrderCommand command) {
// application层的职责:
// 1. 协调domain对象和domain服务
// 2. 控制事务边界
// 3. 发布领域事件
// 注意:application层自己不写业务规则!
// 1. 获取商品信息(这里用productService查询)
List<OrderItemCommand> itemsWithPrice = command.getItems().stream()
.map(item -> {
Money price = productService.getPrice(item.getProductId());
return new OrderItemCommand(item.getProductId(), item.getQuantity(), price);
})
.collect(toList());
// 2. 创建订单(业务规则在Order里,不在这里)
Order order = Order.create(
new CustomerId(command.getCustomerId()),
itemsWithPrice
);
// 3. 保存
orderRepository.save(order);
// 4. 发布领域事件(通知库存服务、通知邮件服务等)
order.getDomainEvents().forEach(eventPublisher::publish);
return order.getId();
}
}
// =============================================
// domain层:业务的心脏
// =============================================
package com.example.order.domain.model;
// 值对象:OrderId(不可变,相等性由值决定)
public final class OrderId {
private final String value;
public OrderId(String value) {
if (value == null || value.isBlank()) {
throw new DomainException("OrderId不能为空");
}
this.value = value;
}
public static OrderId generate() {
return new OrderId(UUID.randomUUID().toString());
}
public String getValue() { return value; }
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof OrderId)) return false;
return value.equals(((OrderId) o).value);
}
@Override
public int hashCode() { return value.hashCode(); }
}
// 值对象:Money(金额,不可变)
public final class Money {
public static final Money ZERO = new Money(BigDecimal.ZERO, "CNY");
private final BigDecimal amount;
private final String currency;
public Money(BigDecimal amount, String currency) {
if (amount.compareTo(BigDecimal.ZERO) < 0) {
throw new DomainException("金额不能为负数");
}
this.amount = amount.setScale(2, RoundingMode.HALF_UP);
this.currency = currency;
}
public Money add(Money other) {
if (!this.currency.equals(other.currency)) {
throw new DomainException("不同货币不能相加");
}
return new Money(this.amount.add(other.amount), this.currency);
}
public Money multiply(int multiplier) {
return new Money(this.amount.multiply(BigDecimal.valueOf(multiplier)), this.currency);
}
}
// 聚合根:Order
public class Order extends AggregateRoot {
private OrderId id;
private CustomerId customerId;
private OrderStatus status;
private List<OrderItem> items;
private Money totalAmount;
private LocalDateTime createdAt;
// 私有构造器,强制使用工厂方法
private Order() {}
public static Order create(CustomerId customerId, List<OrderItemCommand> commands) {
// 业务规则:订单不能为空
if (commands == null || commands.isEmpty()) {
throw new DomainException("订单至少需要一个商品");
}
Order order = new Order();
order.id = OrderId.generate();
order.customerId = customerId;
order.status = OrderStatus.CREATED;
order.createdAt = LocalDateTime.now();
order.items = commands.stream()
.map(cmd -> new OrderItem(cmd.getProductId(), cmd.getQuantity(), cmd.getPrice()))
.collect(Collectors.toList());
order.totalAmount = order.calculateTotal();
// 发布领域事件
order.registerEvent(new OrderCreatedEvent(order.id, order.customerId, order.totalAmount));
return order;
}
public void pay(PaymentId paymentId) {
if (this.status != OrderStatus.CREATED) {
throw new DomainException(
String.format("订单[%s]状态为[%s],无法支付", id.getValue(), status)
);
}
this.status = OrderStatus.PAID;
this.registerEvent(new OrderPaidEvent(this.id, paymentId));
}
public void cancel(CancelReason reason) {
if (!this.isCancellable()) {
throw new DomainException(
String.format("订单[%s]状态为[%s],无法取消", id.getValue(), status)
);
}
this.status = OrderStatus.CANCELLED;
this.registerEvent(new OrderCancelledEvent(this.id, reason));
}
// 业务规则:什么情况能取消
private boolean isCancellable() {
return this.status == OrderStatus.CREATED || this.status == OrderStatus.PAID;
}
private Money calculateTotal() {
return items.stream()
.map(OrderItem::getSubtotal)
.reduce(Money.ZERO, Money::add);
}
// 只读getter,没有任何setter
public OrderId getId() { return id; }
public OrderStatus getStatus() { return status; }
public Money getTotalAmount() { return totalAmount; }
}
// =============================================
// infrastructure层:技术实现
// =============================================
package com.example.order.infrastructure.persistence;
// 数据库对象(DO):和数据库表结构对应
@Entity
@Table(name = "t_order")
public class OrderDO {
@Id
private String id;
private String customerId;
private String status;
private BigDecimal totalAmount;
private String currency;
private LocalDateTime createdAt;
@OneToMany(cascade = CascadeType.ALL, mappedBy = "orderId")
private List<OrderItemDO> items;
// getter/setter...
}
// 仓储实现:负责Domain对象和DO之间的转换
@Repository
public class OrderRepositoryImpl implements OrderRepository {
@Autowired
private OrderJpaRepository jpa;
@Override
public void save(Order order) {
OrderDO orderDO = this.toDataObject(order);
jpa.save(orderDO);
}
@Override
public Optional<Order> findById(OrderId orderId) {
return jpa.findById(orderId.getValue())
.map(this::toDomainObject);
}
// 这里的转换逻辑可能很复杂,但这复杂性不会污染domain层
private OrderDO toDataObject(Order order) {
OrderDO orderDO = new OrderDO();
orderDO.setId(order.getId().getValue());
orderDO.setStatus(order.getStatus().name());
// ...
return orderDO;
}
private Order toDomainObject(OrderDO orderDO) {
// 通过反射或其他方式重建Domain对象
// 这是infrastructure层最难的部分
// ...
}
}
第八章:这样设计一定是好的吗?------架构的代价
这里我要说一句实话:DDD不是银弹,它有代价。
DDD的代价:
1. 更多的类
同样一个订单,你现在有:
Order(聚合根)OrderItem(实体)OrderId(值对象)Money(值对象)OrderDO(数据库对象)OrderDTO(传输对象)OrderResponse(HTTP响应)CreateOrderCommand(命令)OrderMapper(转换器)
在MVC里,你只有Order和一个DTO。
2. 更陡的学习曲线
团队里如果有人不理解DDD,他会本能地把逻辑写在Service里,破坏你精心设计的模型。
3. 前期投入更多
设计聚合边界是很花脑子的事情。一开始设计错了,后期改动代价很大。
什么时候用DDD?
业务复杂度
高 │ ③ 谨慎用MVC ④ 必须用DDD
│ (会撑不住) (业务规则复杂)
│
低 │ ① 用MVC就够了 ② 杀鸡用牛刀
│ (简单CRUD) (过度设计)
└────────────────────────────────────
低 高
团队规模/协作复杂度
一个CRUD的管理后台,用DDD是在折磨自己。
一个金融交易系统、一个复杂的电商系统,不用DDD迟早崩溃。
第九章:DDD进化------事件驱动与CQRS
DDD之后,架构继续进化。
CQRS(命令查询职责分离)
DDD带来了一个新问题:聚合根对读操作很不友好。
你要显示一个"订单列表"页面,里面要显示:订单号、商品名、用户名、支付时间、物流状态......
这些信息分散在Order、Product、User、Shipment四个聚合里,你要查四次,还要拼装,性能很差。
CQRS的解决方案:写和读,用完全不同的模型。
java
// 写模型:严格的聚合根,保证业务规则
// (还是上面那个Order)
// 读模型:扁平的、专门为查询优化的对象
// 这个模型不需要任何业务规则,只需要好查
public class OrderListItemView {
// 一张宽表,包含所有显示需要的字段
private String orderId;
private String orderStatus;
private String customerName; // 从User聚合来的
private String productName; // 从Product聚合来的
private BigDecimal totalAmount;
private String trackingNumber; // 从Shipment聚合来的
private LocalDateTime createdAt;
// 直接就是显示需要的字段,不需要任何转换
}
// 查询服务:直接查宽表或者查缓存,不走聚合根
@Service
public class OrderQueryService {
@Autowired
private OrderViewRepository viewRepository; // 指向一个读库或者物化视图
public PageResult<OrderListItemView> listOrders(OrderListQuery query) {
return viewRepository.findByCustomerId(
query.getCustomerId(),
query.getPageable()
);
}
}
// 写的时候通过领域事件更新读模型
@Component
public class OrderViewEventHandler {
@EventListener
public void onOrderCreated(OrderCreatedEvent event) {
// 更新读模型(物化视图)
OrderListItemView view = buildView(event);
viewRepository.save(view);
}
@EventListener
public void onOrderPaid(OrderPaidEvent event) {
viewRepository.updateStatus(event.getOrderId(), "PAID");
}
}
事件溯源(Event Sourcing)
更激进的想法:不存储对象的最终状态,而是存储所有的事件,通过重放事件来恢复状态。
java
// 传统方式:存储当前状态
// t_order表里有一行:status=PAID
// 事件溯源:存储所有事件
// t_order_events表里有:
// 1. OrderCreated { orderId: "001", customerId: "u001", amount: 100 }
// 2. OrderPaid { orderId: "001", paymentId: "p001" }
// 恢复Order对象的方式:重放所有事件
public class Order {
public static Order rebuild(List<DomainEvent> events) {
Order order = new Order();
for (DomainEvent event : events) {
order.apply(event); // 逐个应用事件,恢复到最终状态
}
return order;
}
private void apply(DomainEvent event) {
if (event instanceof OrderCreatedEvent e) {
this.id = e.getOrderId();
this.status = OrderStatus.CREATED;
this.totalAmount = e.getTotalAmount();
} else if (event instanceof OrderPaidEvent e) {
this.status = OrderStatus.PAID;
} else if (event instanceof OrderCancelledEvent e) {
this.status = OrderStatus.CANCELLED;
}
}
}
事件溯源的好处:
- 完整的审计日志(金融系统的最爱)
- 可以回放到任意历史时刻的状态
- 天然适合CQRS
第十章:未来------大模型时代的架构
现在我们站在2026年,大模型已经改变了一些事情:
1. 意图驱动架构(Intent-Driven Architecture)
以前:用户点击"取消订单"按钮 → HTTP POST /orders/{id}/cancel
未来:用户说"帮我把昨天买的那双鞋的订单取消掉" → AI理解意图 → 调用cancel
java
// 意图层(新增的一层)
public class OrderIntentHandler {
private final LLMService llm;
private final OrderApplicationService orderService;
public String handleNaturalLanguage(String userId, String userInput) {
// 用大模型理解意图
OrderIntent intent = llm.parseIntent(userInput, OrderIntent.class);
// 还是落回到标准的应用层
return switch (intent.getType()) {
case CANCEL -> {
orderService.cancel(intent.getOrderId(), intent.getReason());
yield "好的,您的订单已取消";
}
case QUERY_STATUS -> {
OrderStatus status = orderService.getStatus(intent.getOrderId());
yield "您的订单当前状态是:" + status.getDescription();
}
default -> "抱歉,我不理解您的意图";
};
}
}
2. Domain层更纯粹
大模型可以帮你生成interfaces层和infrastructure层的模板代码,但domain层的业务规则,永远需要人来定义。
这反而让DDD的核心价值更加突出:你最该花时间的,是领域模型的设计。
3. 架构的本质从未改变
从混沌代码,到MVC,到DDD,到事件溯源,到AI驱动------
所有的架构演进,本质上都在做同一件事:
分离关注点,让每一部分只做它该做的事,并且把最重要的业务逻辑保护起来,不受技术变化的影响。
终章:给困惑的你,一句话总结
混沌代码:所有东西混在一起,改一处动全身
MVC:按技术职责分层(Controller/Service/DAO),
解决了技术混乱,但业务逻辑在Service里堆成山
DDD:按业务边界分层,
让业务规则住在业务对象里,
让技术细节(数据库、消息队列、HTTP)
彻底无法污染业务核心
CQRS+事件溯源:读写分离,事件为王,
适合极复杂的业务场景
未来:AI处理输入输出的翻译,
但Domain层永远是人类智慧的结晶
架构,说到底,是人类对抗复杂性的方式。
只要业务还在增长,只要团队还在协作,这场战争就不会停止。
DDD不是终点,它只是目前人类找到的、最好的应对复杂业务的武器之一。