01 — MVC 与 DDD 的思维差异

🧠知识图谱

MVC 架构回顾

传统 MVC(Model-View-Controller)架构是大多数 Java 开发者的起点,典型的三层架构如下:

典型的 MVC 项目结构

复制代码
com.example
├── controller/
│   ├── OrderController.java
│   └── UserController.java
├── service/
│   ├── OrderService.java         ← 所有订单业务逻辑堆在这里
│   └── UserService.java
├── mapper/
│   ├── OrderMapper.java
│   └── UserMapper.java
└── entity/
    ├── Order.java                ← 只有 getter/setter,没有行为
    └── User.java

MVC 的痛点

痛点一:贫血模型导致业务逻辑分散

java 复制代码
// ❌ 贫血模型:Order 只是数据容器
@Data
public class Order {
    private Long id;
    private String status;
    private BigDecimal totalAmount;
    private List<OrderItem> items;
    // 只有 getter/setter,无业务方法
}

// ❌ 业务逻辑全部堆在 Service 层
@Service
public class OrderService {

    public void cancelOrder(Long orderId) {
        Order order = orderMapper.findById(orderId);
        // 状态校验逻辑散落在 Service
        if (!"PENDING".equals(order.getStatus())) {
            throw new BusinessException("只有待支付订单可以取消");
        }
        if (order.getTotalAmount().compareTo(BigDecimal.ZERO) <= 0) {
            throw new BusinessException("金额异常");
        }
        order.setStatus("CANCELLED");
        orderMapper.update(order);
        // 退款逻辑也在这里...
        // 库存归还逻辑也在这里...
        // 通知逻辑也在这里...
    }
}

痛点二:Service 层无限膨胀

随着业务增长,OrderService 可能超过 2000 行,里面混杂着下单、支付、退款、物流、统计等各种逻辑。新人完全不知道从哪里入手。

痛点三:技术与业务语言脱节

java 复制代码
// ❌ 技术人员写的代码(数据库思维)
orderMapper.updateStatusByIdAndStatus(orderId, "PAID", "PENDING");
userMapper.updateBalance(userId, amount.negate());

// 业务专家说的话:
// "将待支付订单确认支付后,扣减用户账户余额"
// (完全对不上)

痛点四:跨层依赖难以测试

java 复制代码
// ❌ Service 依赖 Mapper,测试必须 Mock 数据库
@Test
void testCancelOrder() {
    // 需要 Mock OrderMapper、UserMapper、InventoryMapper...
    // 单元测试成本极高
}

DDD 的核心理念

什么是领域驱动设计?

DDD(Domain-Driven Design) 是 Eric Evans 在 2003 年提出的软件设计方法论:

以业务领域为核心,用代码精确表达业务模型,让软件能"说"业务语言。

DDD 的两大核心设计

设计层次 关注点 主要工具
战略设计 系统如何划分?谁和谁在一起? 限界上下文、通用语言、上下文映射
战术设计 一个上下文内部如何实现? 实体、值对象、聚合、领域服务、领域事件

DDD 四层架构 vs MVC 三层架构

DDD 典型的四层架构如下,领域层是绝对核心,其他层都围绕它展开:

对比两种架构的核心差异:MVC 按技术职责 分层,DDD 按业务价值分层,领域层是绝对核心。

💡 最关键的区别:MVC 的 Service 是业务逻辑的"容器",DDD 的领域层中每个对象都"自己负责自己的业务规则",应用层只做编排。

典型的 DDD 项目结构

与 MVC 按技术职责组织代码不同,DDD 按业务边界 + 分层职责组织,同一个业务概念(如 Order)的所有相关代码都放在同一个上下文内:

复制代码
com.example.order/                       ← 订单限界上下文(独立模块)
├── interfaces/                          ← 接口层:对外暴露
│   ├── rest/
│   │   ├── OrderController.java         ← HTTP 接口
│   │   └── dto/
│   │       ├── PlaceOrderRequest.java   ← 入参 DTO
│   │       └── OrderResponse.java       ← 出参 DTO
│   └── event/
│       └── PaymentEventListener.java    ← 监听外部 MQ 事件
│
├── application/                         ← 应用层:用例编排
│   ├── service/
│   │   └── OrderApplicationService.java ← 编排领域对象,不含 if/else 业务判断
│   └── command/
│       └── PlaceOrderCommand.java       ← 用例输入对象
│
├── domain/                              ← 领域层:核心业务(零框架依赖)
│   ├── model/
│   │   ├── Order.java                   ← ★ 聚合根,包含业务行为
│   │   ├── OrderItem.java               ← 实体
│   │   └── vo/
│   │       ├── OrderId.java             ← 值对象:唯一标识
│   │       ├── Money.java               ← 值对象:金额
│   │       └── OrderStatus.java         ← 枚举:状态流转规则
│   ├── service/
│   │   └── OrderPricingService.java     ← 领域服务:跨聚合的业务规则
│   ├── event/
│   │   └── OrderPlacedEvent.java        ← 领域事件(过去时命名)
│   └── repository/
│       └── OrderRepository.java         ← 仓储接口(定义在领域层)
│
└── infrastructure/                      ← 基础设施层:技术实现细节
    ├── persistence/
    │   ├── OrderRepositoryImpl.java      ← 仓储实现(实现领域层接口)
    │   ├── mapper/
    │   │   └── OrderMapper.java          ← MyBatis Mapper
    │   └── po/
    │       └── OrderPO.java              ← 数据库持久化对象
    └── acl/
        └── InventoryServiceAdapter.java  ← 防腐层:隔离库存服务调用

与 MVC 结构的直观对比:

对比维度 MVC 结构 DDD 结构
组织方式 按技术层切分(controller/service/dao) 按业务边界 + 分层职责
Order 相关代码 散落在各层目录下 全部收敛在 order/ 上下文模块内
业务规则位置 OrderService.java(堆积) Order.java(聚合根内部,自我管理)
新人理解业务 需要跨多个目录来回跳 进入 domain/model/ 即可看清业务全貌
添加新业务 修改多个层的文件 先在领域层添加行为,再在应用层编排

贫血模型 vs 充血模型

这是 MVC 和 DDD 最直观的代码差异:

MVC:贫血模型(Anemic Domain Model)

java 复制代码
// ❌ 贫血:对象只有数据,没有行为
@Data
public class Order {
    private Long id;
    private String status;          // "PENDING", "PAID", "CANCELLED"
    private BigDecimal totalAmount;
    private List<OrderItem> items;
}

// 所有行为都在 Service 中
@Service
public class OrderService {
    public void pay(Long orderId, BigDecimal amount) { /* ... */ }
    public void cancel(Long orderId) { /* ... */ }
    public void addItem(Long orderId, Long productId) { /* ... */ }
}

DDD:充血模型(Rich Domain Model)

java 复制代码
// ✅ 充血:对象既有数据,也有业务行为
public class Order {
    private OrderId id;
    private OrderStatus status;
    private Money totalAmount;
    private List<OrderItem> items;

    // ✅ 业务行为在领域对象内部
    public void pay(Money amount) {
        if (!this.status.canPay()) {
            throw new OrderException("当前状态不可支付: " + this.status);
        }
        if (!amount.equals(this.totalAmount)) {
            throw new OrderException("支付金额与订单金额不符");
        }
        this.status = OrderStatus.PAID;
        // 发布领域事件
        DomainEvents.publish(new OrderPaidEvent(this.id, amount));
    }

    public void cancel(String reason) {
        if (!this.status.canCancel()) {
            throw new OrderException("当前状态不可取消: " + this.status);
        }
        this.status = OrderStatus.CANCELLED;
        DomainEvents.publish(new OrderCancelledEvent(this.id, reason));
    }

    public void addItem(Product product, int quantity) {
        this.items.stream()
            .filter(item -> item.getProductId().equals(product.getId()))
            .findFirst()
            .ifPresentOrElse(
                item -> item.increaseQuantity(quantity),
                () -> items.add(OrderItem.create(product, quantity))
            );
        this.totalAmount = calculateTotal();
    }
}

思维转换对照

核心思维转换清单

问题 MVC 思维 DDD 思维
从哪里开始设计? 数据库表 业务领域
业务逻辑放在哪里? Service 层 领域对象内部
如何命名? updateOrderStatus() order.pay() / order.cancel()
如何划分模块? 按技术层(controller/service/dao) 按业务边界(订单/用户/库存)
对象有哪些? 基本等于数据库表 实体、值对象、聚合、服务...
如何处理跨模块调用? Service 互相注入 通过领域事件解耦

代码对比示例

场景:用户下单

MVC 写法:

java 复制代码
@Service
@Transactional
public class OrderService {

    @Autowired private OrderMapper orderMapper;
    @Autowired private ProductMapper productMapper;
    @Autowired private UserMapper userMapper;
    @Autowired private InventoryMapper inventoryMapper;

    public Long createOrder(Long userId, Long productId, int quantity) {
        // 1. 查用户
        UserDO user = userMapper.findById(userId);
        if (user == null) throw new RuntimeException("用户不存在");

        // 2. 查商品
        ProductDO product = productMapper.findById(productId);
        if (product == null) throw new RuntimeException("商品不存在");
        if (!"ON_SALE".equals(product.getStatus())) throw new RuntimeException("商品已下架");

        // 3. 检查库存
        InventoryDO inventory = inventoryMapper.findByProductId(productId);
        if (inventory.getStock() < quantity) throw new RuntimeException("库存不足");

        // 4. 计算金额
        BigDecimal totalAmount = product.getPrice().multiply(BigDecimal.valueOf(quantity));

        // 5. 创建订单 DO
        OrderDO order = new OrderDO();
        order.setUserId(userId);
        order.setProductId(productId);
        order.setQuantity(quantity);
        order.setTotalAmount(totalAmount);
        order.setStatus("PENDING");
        order.setCreateTime(LocalDateTime.now());
        orderMapper.insert(order);

        // 6. 扣减库存
        inventoryMapper.decreaseStock(productId, quantity);

        return order.getId();
    }
}

DDD 写法:

java 复制代码
// ✅ 应用服务:只负责协调,不含业务逻辑
@ApplicationService
@Transactional
public class PlaceOrderCommandHandler {

    private final OrderRepository orderRepository;
    private final ProductRepository productRepository;

    public OrderId handle(PlaceOrderCommand cmd) {
        // 1. 加载聚合
        Product product = productRepository.findById(cmd.getProductId())
            .orElseThrow(() -> new ProductNotFoundException(cmd.getProductId()));

        // 2. 创建订单聚合(业务规则在领域对象内部)
        Order order = Order.place(
            OrderId.generate(),
            cmd.getUserId(),
            product,
            cmd.getQuantity()
        );

        // 3. 持久化
        orderRepository.save(order);

        return order.getId();
    }
}

// ✅ 领域对象:包含业务规则
public class Order {

    // 工厂方法,包含所有创建规则
    public static Order place(OrderId id, UserId userId, Product product, int quantity) {
        product.ensureOnSale();                    // 商品状态校验(在 Product 内部)
        product.reserveStock(quantity);            // 占用库存(在 Product 内部)

        Money totalAmount = product.getPrice().multiply(quantity);

        Order order = new Order(id, userId, OrderStatus.PENDING, totalAmount);
        order.addItem(product, quantity);

        // 发布领域事件
        order.publishEvent(new OrderPlacedEvent(id, userId, totalAmount));

        return order;
    }
}

什么时候用 DDD

⚠️ DDD 不是银弹,引入它是有成本的。

推荐使用 DDD 的场景

✅ 适合 DDD ❌ 不适合 DDD
复杂的电商系统(订单、库存、促销、支付) 简单后台管理系统
金融系统(账户、交易、风控) 数据报表平台
供应链系统(采购、仓储、物流) 纯 CRUD 的配置管理系统
需要长期演进的核心业务系统 需要快速交付的小项目
微服务架构需要划分边界 业务逻辑较少的中台服务
相关推荐
oldking呐呐2 小时前
MySQL从建库到删库跑路 -- 3.库的操作
后端·mysql
疯狂成瘾者2 小时前
对比JAR 包部署 vs Docker 部署方式
java·docker·jar
丑八怪大丑2 小时前
Java范型
java·开发语言
加藤不太惠2 小时前
Nacos简单实用集群创建
java·开发语言·nacos
空中海2 小时前
第一篇:入门篇 — 认识 Spring Boot 与基础开发
java·spring boot·后端
栈位迁移中2 小时前
二十一、Spring Framework 详细知识点文档
后端
马艳泽2 小时前
Maven 编译时生成、纯静态文档、不能调试、零侵入、不用运行项目的api文档
后端
RainCity2 小时前
Java Swing 自定义组件库分享(三)
java·笔记
泰式大师2 小时前
从“记忆”到“项目 Wiki”:我在 SkillLite 里实现了一套 Markdown-only LLM Wiki 自动维护机制
后端