Skip to content
第 1 章 架构 ⏱ 10 分钟阅读

第 1 章:DDD 入门 ​

学习目标 ​

  • 理解 DDD 解决的问题与传统开发的差异
  • 掌握 DDD 的核心概念:领域、子域、限界上下文
  • 学会用统一语言沟通业务
  • 识别 DDD 落地的常见坑

一、为什么需要 DDD? ​

传统开发最大的问题是业务与技术脱节:产品说"下单",代码里却是 OrderInfoDao.insert(),同名不同义,改需求到处翻代码。

java
// 传统贫血模型:业务散落在 Service 里,实体只是数据载体
public class Order {
    private Long id;
    private String status;        // 状态:已创建/已支付/已发货...
    private BigDecimal amount;
    // 没有业务方法,只有 getter/setter
}

@Service
public class OrderService {
    public void pay(Long orderId) {
        Order order = orderDao.findById(orderId).get();
        if (!"CREATED".equals(order.getStatus())) {
            throw new RuntimeException("订单状态不对");
        }
        order.setStatus("PAID");
        orderDao.save(order);
        // 状态判断、规则校验散落各处
    }
}

⚠️ 坑 1:贫血模型把所有逻辑塞进 Service,后期 Service 膨胀成"上帝类",几千行无法维护。

二、DDD 的核心思想 ​

DDD(Domain-Driven Design,领域驱动设计)强调业务复杂度是软件的核心,代码必须直接表达业务。两大支柱:

  1. 统一语言(Ubiquitous Language):开发、产品、测试用同一套词汇
  2. 模型驱动设计:代码结构 = 业务结构
java
// DDD 充血模型:实体自带业务逻辑
public class Order {
    private OrderId id;
    private OrderStatus status;     // 枚举替代魔法字符串
    private Money amount;

    public void pay() {
        if (status != OrderStatus.CREATED) {
            throw new OrderStatusException("订单状态不对:" + status);
        }
        this.status = OrderStatus.PAID;
        // 状态变更的规则封装在实体内部,外部只调用
    }
}

⚠️ 坑 2:status 用 String 还是 Enum?必须用枚举。魔术字符串无法编译期检查,会出现 "PAID" 和 "paid" 共存的灵异 bug。

三、领域与子域 ​

把整个业务范围叫"领域",按重要性拆成子域:

text
电商领域
├── 核心子域(核心竞争力)
│   ├── 商品(定价、库存)
│   └── 订单(下单、支付)
├── 支撑子域(必要但不独特)
│   └── 物流(发货、配送)
└── 通用子域(谁都能用)
    └── 认证、通知
java
// 子域在代码里体现为独立的 Bounded Context(限界上下文)
com.example.ecommerce
├── catalog/          // 商品上下文
│   ├── domain/
│   ├── application/
│   └── infrastructure/
├── order/            // 订单上下文
│   ├── domain/
│   ├── application/
│   └── infrastructure/
└── delivery/         // 物流上下文

⚠️ 坑 3:把"通用子域"当核心子域自己造轮子,典型反例:自己实现一套 OAuth2。直接用成熟方案(Authorization Server、Keycloak)省一个月。

四、限界上下文(Bounded Context) ​

每个子域独立成一个限界上下文,上下文内部模型一致,跨上下文靠**防腐层(ACL)**翻译。

java
// 订单上下文里的用户(只关心 ID 和昵称)
public class Buyer {
    private final UserId userId;
    private final String nickname;
}

// 客服上下文里的用户(关心联系方式、订单历史)
public class Customer {
    private final UserId userId;
    private final String phone;
    private final List<OrderId> recentOrders;
}

// 防腐层:跨上下文调用时翻译
@Component
public class CustomerTranslator {
    public Buyer toBuyer(Customer customer) {
        return new Buyer(customer.getUserId(), customer.getNickname());
    }
}

⚠️ 坑 4:跨上下文直接传数据库实体,导致两边改了字段互相影响。必须转换,一次转换写 10 行代码,换来后期不踩雷。

五、统一语言落地 ​

把会议室讨论的术语,变成代码里的类名、字段名、枚举名:

java
// 业务术语:商品 SKU、SKU 组合、套装价
public class Sku {
    private final SkuId skuId;
    private final BigDecimal price;
    private final Stock stock;
}

public class SkuCombination {        // 套装 = 多个 SKU 的组合
    private final List<SkuId> skuIds;
    private final Money bundlePrice;
}

会议里大家说"套装",代码里就是 SkuCombination,没有"组合商品"和"套装"两个名字并存。

六、DDD 不是什么 ​

  • ❌ DDD 不是银弹,CRUD 报表类业务用 DDD 反而过度设计
  • ❌ DDD 不是架构,整洁架构/六边形/洋葱是 DDD 落地时搭配的架构
  • ❌ DDD 不是必须微服务,单体里照样能用 DDD

⚠️ 坑 5:一上来就拆 5 个微服务,DDD 战术设计还没熟悉。先单体 + DDD,业务跑通再拆。

本章小结 ​

要点关键
贫血模型只有数据没有业务,逻辑全在 Service
充血模型实体自带业务方法,状态变更受控
统一语言业务术语 = 类名/字段名
领域/子域核心、支撑、通用三类
限界上下文业务边界,内部模型一致
防腐层跨上下文转换,防止模型泄漏

动手练习 ​

  1. 领域画布:挑一个你熟悉的业务(食堂打饭、图书馆借书),列出 3 个核心子域和 5 个术语
  2. 充血改造:把上面 OrderService.pay 改写为 Order.pay(),并加上 OrderStatus 枚举
  3. 上下文划分:把电商系统拆成"商品/订单/支付/物流"四个包,每个包独立 domain/application/infrastructure 目录

下一章:第 2 章:战略设计 →

本站基于 VitePress 构建 · 由 StackHub 团队维护