Java DDD(领域驱动设计)
📅 2026/7/24 1:16:38
👁️ 阅读次数
📝 编程学习
一句话核心:DDD(领域驱动设计)不是一套框架,而是一种代码组织哲学。它要求你用代码直接翻译业务语言,而不是把业务逻辑散落在增删改查(CRUD)里。
对于Java开发者,最经典的DDD范式就是**“四层架构” + “六大战术模式”。为了让你秒懂,我们把项目类比为“一家餐厅”**:
- 表现层(Controller)= 服务员(接收顾客指令)。
- 应用层(Application)= 大堂经理(调度任务,发号施令)。
- 领域层(Domain)= 厨师长 + 菜谱(核心资产,业务规则所在)。
- 基础设施层(Infrastructure)= 冰箱、灶台(数据库、Redis、MQ)。
下面我直接给你展示标准的Java DDD包结构和核心代码范式,一看就懂。
1. 标准的Java DDD包结构(范式骨架)
com.example.order/ ├── interfaces/ # 表现层(处理HTTP/RPC) │ └── OrderController.java ├── application/ # 应用层(用例编排,无业务逻辑) │ ├── service/ # 应用服务(如:OrderApplicationService) │ └── dto/ # 数据传输对象(Request/Response) ├── domain/ # 领域层(核心!只依赖Java本身,不依赖Spring/DB) │ ├── model/ # 领域模型 │ │ ├── aggregate/ # 聚合根(如:Order) │ │ ├── entity/ # 实体(如:OrderItem) │ │ └── vo/ # 值对象(如:Address, Money) │ ├── service/ # 领域服务(处理跨多个实体的复杂业务) │ └── repository/ # 仓储接口(定义“我需要什么数据”,不实现) └── infrastructure/ # 基础设施层(实现Domain层的接口) ├── repository/ # 仓储实现(JPA/MyBatis) └── config/ # 配置类2. 六大战术模式(Java代码实战)
为了便于理解,我们模拟一个**“下单”**场景:必须验证地址,且订单总价要打折。
① 值对象 (Value Object) —— 用record或不可变类
特点:没有唯一ID,通过属性值判断相等性。封装业务规则。
// 值对象:地址publicrecordAddress(Stringprovince,Stringcity,Stringdetail){publicAddress{// 业务规则:地址不能为空,直接写在构造方法里if(province==null||city==null){thrownewIllegalArgumentException("省市必填");}}}// 值对象:金额publicclassMoney{privatefinalBigDecimalamount;privatefinalStringcurrency;// 通常用枚举// 封装加法逻辑,而不是在Service里用BigDecimal算publicMoneyadd(Moneyother){...}}② 实体 (Entity) —— 有唯一ID,可变
@Entity// 注意:这里的注解是JPA的,但DDD要求Domain层纯POJO,所以通常把这个类放在infrastructure层做映射,或者在Domain层不加@Table注解。publicclassOrderItem{privateLongitemId;// 唯一标识privateStringproductName;privateIntegerquantity;privateMoneyprice;// 引用值对象}③ 聚合根 (Aggregate Root) ——DDD最重要的范式
聚合根是外部访问聚合的唯一入口。比如“订单(Order)”和“订单项(OrderItem)”是整体,外部只能操作Order,不能直接修改OrderItem。
// 领域层 - 聚合根publicclassOrder{privateLongorderId;// 唯一标识privateAddressshipAddress;// 值对象privateList<OrderItem>items;// 实体集合privateOrderStatusstatus;// 核心业务方法(充血模型):业务逻辑写在实体内部,而不是Service里!publicvoidplaceOrder(){// 规则1:下单必须校验地址(封装在领域内部)if(this.shipAddress==null){thrownewBusinessException("缺少收货地址");}// 规则2:计算总价并打折(满100减10)Moneytotal=calculateTotal();if(total.amount().compareTo(newBigDecimal("100"))>=0){// 打折逻辑...}this.status=OrderStatus.PLACED;// 发布领域事件(可选)DomainEventPublisher.publish(newOrderPlacedEvent(this.orderId));}// 计算总价也不允许Service插手,由实体自己算privateMoneycalculateTotal(){returnitems.stream().map(OrderItem::getPrice).reduce(Money::add).orElse(...);}}④ 仓储接口 (Repository) ——依赖倒置的核心
Domain层定义“我要什么”,Infrastructure层实现“怎么拿”。
// domain/repository/OrderRepository.java (接口)publicinterfaceOrderRepository{OrderfindById(Longid);voidsave(Orderorder);// 传入聚合根,整个聚合一起保存}// infrastructure/repository/OrderRepositoryImpl.java (实现)@RepositorypublicclassOrderRepositoryImplimplementsOrderRepository{@AutowiredprivateOrderJpaMappermapper;// MyBatis或JPA@Overridepublicvoidsave(Orderorder){// 1. 保存Order主表// 2. 保存OrderItem子表(全部替换策略)// 领域层不知道底层是MySQL还是Redis}}⑤ 领域服务 (Domain Service)
当业务逻辑不属于某个具体的实体时使用(比如转账涉及两个账户)。
// domain/service/TransferService.javapublicclassTransferService{publicvoidtransfer(Accountfrom,Accountto,Moneyamount){from.debit(amount);// 实体自己的方法to.credit(amount);}}⑥ 应用层 (Application Service) ——极薄的一层
只做三件事:拿数据、调领域方法、存数据。绝对不写 if-else 业务判断。
// application/service/OrderAppService.java@Service@TransactionalpublicclassOrderAppService{privatefinalOrderRepositoryorderRepository;// 用例:用户下单publicLongplaceOrder(PlaceOrderCommandcmd){// 1. 从仓储拿聚合根(如果是新建则new)Orderorder=newOrder();// 2. 调用领域方法(业务逻辑进入领域层)order.placeOrder();// 3. 仓储保存orderRepository.save(order);// 4. 返回结果returnorder.getOrderId();}}3. 必须遵守的“铁律”(DDD Java范式红线)
| 层级 | 是否允许依赖Spring注解 | 是否允许依赖数据库 | 职责 |
|---|---|---|---|
| 领域层 (Domain) | ❌ 禁止(纯Java POJO) | ❌ 禁止 | 只写interface定义规则,写业务逻辑方法 |
| 应用层 (Application) | ✅ 允许 (@Service) | ❌ 禁止 | 编排用例(调用领域方法),事务发起点 |
| 基础设施层 (Infra) | ✅ 允许 (@Repository) | ✅ 允许 | 实现接口,处理SQL、Redis、MQ |
| 表现层 (Interfaces) | ✅ 允许 (@RestController) | ❌ 禁止 | 参数校验、返回JSON |
为什么领域层禁止用Spring注解?
因为DDD追求**“业务逻辑与技术解耦”**。如果@Autowired进了Domain,换用Quarkus或单元测试时就废了。Domain应该是可以脱离Spring,直接用new Order().placeOrder()就能跑单元测试的。
4. 贫血模型 vs 充血模型(一眼辨别真假DDD)
- 传统CRUD(贫血):
Order类只有getter/setter,所有if/else全在OrderServiceImpl里。→这是假DDD。 - DDD范式(充血):
Order类拥有placeOrder()、cancel()、calculateTotal()方法。OrderAppService只有寥寥几行调用。→这是真DDD。
总结(记住这张脑图)
表现层接收请求 →应用层从基础设施层拿到仓储接口的实现 → 交给领域层的聚合根执行业务方法(内部包含实体和值对象的规则) → 应用层再通过仓储存回去。
如果只是简单的增删改查,请不要用DDD(杀鸡用牛刀)。但如果业务逻辑极其复杂(如电商、金融、医疗),这个Java范式就是防止代码腐烂的最强武器。记住核心口诀:“让代码说业务的话,让数据库滚去基础设施层”。
编程学习
技术分享
实战经验