DDD架构设计的本质与演进

你是世界顶尖架构师,已经做到了返璞归真的境界,你擅长用通俗的语言,将架构的本质,讲给困惑的小白,使其醍醐灌顶。

什么是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,你只需要换掉OrderRepositoryImpldomain层的代码一行不动


第七章:四层各司其职的完整代码

让我用一个完整的"下订单"流程,串联起所有四层:

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不是终点,它只是目前人类找到的、最好的应对复杂业务的武器之一。

相关推荐
TT_Close4 天前
AI 生图不听话?给它戴上“封面金箍”,分分钟搞定全平台封面
ai编程·领域驱动设计
G探险者21 天前
架构演进之 DDD:从 CRUD 到领域驱动设计
后端·架构·领域驱动设计
唯一世22 天前
务实 DDD:在 Spring Boot 中平衡“纯粹性”与“开发效率”的落地实践
领域驱动设计
递归尽头是星辰23 天前
DDD 认知升级:从单服务战术落地,到分布式中台战略全景
领域驱动设计·架构设计·微服务拆分·ddd 落地实践·ddd 战略战术
狼爷1 个月前
AI编程狂飙时代:别被Vibe Coding毁了系统,DDD+SDD才是下一代稳健开发范式
ai编程·领域驱动设计
Duang1 个月前
从零推导指数估值模型 —— 一个三因子打分系统的设计思路
数据分析·领域驱动设计
canonical_entropy1 个月前
反直觉的软件设计洞察:为什么你可能想不到它们
后端·aigc·领域驱动设计
canonical_entropy1 个月前
DDD 概念澄清:那些教程不会告诉你的事
后端·低代码·领域驱动设计
rolt2 个月前
DDD岁月史书之二:分层架构是DDD提出的吗
架构·产品经理·uml·领域驱动设计