我从23年开始在公司的技术团队里落地DDD,领域驱动设计开发。落地过程中有一个问题反复被团队成员问到:基础设施层为什么反过来要去依赖领域层?
这个问题问得好。因为从这个问题入手,能一次性把DIP、IoC、DI这三个经常被混淆的概念理清楚。
打开我们项目基础设施模块的pom.xml,能看到这样的依赖声明:
XML
<dependency>
<groupId>com.example</groupId>
<artifactId>erp-domain</artifactId>
</dependency>
基础设施模块依赖领域模块。再看领域模块的pom.xml,里面没有任何对基础设施模块的依赖。这跟很多人的直觉是反着的:基础设施层提供数据库访问、外部接口调用这些底层能力,按理说应该是业务层依赖它,怎么反过来了?
要理解这个设计,得从传统分层架构的问题说起。
传统分层架构的依赖方向
传统三层架构里,调用方向和依赖方向是一致的。Controller调用Service,Service调用DAO,每一层的代码都要import下一层的类。反映到Maven模块上,就是service模块依赖dao模块。
这个结构用了很多年,大部分项目也没出什么大问题。问题出在哪呢?
Service层写着业务逻辑,同时它的代码里充满了DAO层的类引用。UserDao、LambdaQueryWrapper、selectPage这些跟数据库操作相关的API,直接出现在业务代码中。业务逻辑和持久化技术搅在一起。
这在小项目里不是事儿。一旦项目规模上去了,比如要把某个模块的持久化从MyBatis换成JPA,或者某些数据要从MySQL迁到Elasticsearch,你会发现需要改的不只是DAO层,Service层的代码也得动,因为Service里到处import着DAO层的类。
业务逻辑是一个系统里最核心、最应该保持稳定的部分,但在传统分层架构里,它依赖了技术实现细节。技术细节一变,业务代码跟着变。 依赖方向是反的。

DDD项目里的依赖方向
我们的DDD项目按照领域驱动设计拆成了多个Maven模块:
Markdown
erp-server (启动模块,依赖所有其他模块)
erp-facade (接口层,对外暴露HTTP/RPC接口)
erp-application (应用层,编排业务流程)
erp-domain (领域层,核心业务逻辑)
erp-infrastructure (基础设施层,数据库访问、外部接口调用)
erp-common (公共模块,通用工具和命令对象)
关键在领域层和基础设施层的依赖关系。领域层定义仓储接口,基础设施层实现这些接口。
领域层里的仓储接口长这样:
Java
package com.example.erp.domain.aggregate.purchase.repository;
public interface PurchaseOrderRepository {
Long save(PurchaseOrderEntity entity);
PurchaseOrderEntity getById(Long id);
Page<PurchaseOrderEntity> getList(PurchaseOrderQuery query, PageInfo pageInfo);
void updateStatus(Long id, String statusCode);
List<PurchaseOrderEntity> getByDateAndTemplateId(String date, Long templateId);
}
这个接口定义在领域模块里。注意它的方法签名:参数和返回值全是领域对象(Entity、Query、PageInfo),没有任何跟数据库相关的类型。它只描述「能做什么」,不涉及「怎么做」。
基础设施层的实现类长这样:
Java
package com.example.erp.infrastructure.repository;
@RequiredArgsConstructor
@Repository
public class PurchaseOrderRepositoryImpl implements PurchaseOrderRepository {
private final PurchaseOrderMapper purchaseOrderMapper;
private final PurchaseOrderItemMapper itemMapper;
private final TransactionTemplate transactionTemplate;
@Override
public Long save(PurchaseOrderEntity entity) {
return transactionTemplate.execute(status -> {
// Entity转PO
PurchaseOrderPO po = PurchaseOrderConverter.INSTANCE.entityToPo(entity);
purchaseOrderMapper.insert(po);
// 保存明细
List<PurchaseOrderItemEntity> items = entity.getItemList();
if (CollectionUtil.isNotEmpty(items)) {
for (PurchaseOrderItemEntity item : items) {
PurchaseOrderItemPO itemPO = PurchaseOrderItemConverter.INSTANCE.entityToPo(item);
itemPO.setOrderId(po.getId());
itemMapper.insert(itemPO);
}
}
return po.getId();
});
}
@Override
public PurchaseOrderEntity getById(Long id) {
PurchaseOrderPO po = purchaseOrderMapper.selectById(id);
return PurchaseOrderConverter.INSTANCE.poToEntity(po);
}
}
实现类在基础设施模块里。这里面全是MyBatis Plus的API(selectById、insert)、PO对象、Converter转换器,这些都是跟持久化技术强相关的代码。
领域层的DomainService使用仓储接口的方式:
Java
package com.example.erp.domain.aggregate.purchase.service.impl;
@RequiredArgsConstructor
@Service
public class PurchaseOrderDomainServiceImpl implements PurchaseOrderDomainService {
// 注入的是接口,不是实现类
private final PurchaseOrderRepository purchaseOrderRepository;
private final OrderNoGeneratorService orderNoGenerator;
@Override
public Long submitOrder(CreatePurchaseOrderCommand command) {
PurchaseOrderEntity entity = PurchaseOrderEntity.createWith(command);
String orderNo = orderNoGenerator.generate(ModuleEnum.PURCHASE);
entity.setOrderNo(orderNo);
return purchaseOrderRepository.save(entity);
}
}
PurchaseOrderDomainServiceImpl在领域模块里,它通过构造器注入了PurchaseOrderRepository接口。它的import列表里不会出现任何infrastructure包下的类,因为领域模块的pom.xml压根没依赖基础设施模块。编译器从物理层面杜绝了领域层对基础设施层的依赖。
这就是我们项目里「基础设施层反向依赖领域层」的全貌。
这是DIP,不是IoC也不是DI
这种模块间的依赖方向设计,对应的是SOLID原则中的依赖反转原则,英文叫Dependency Inversion Principle,简称DIP。Robert C. Martin在1996年提出了这个原则,表述是:
高层模块不应该依赖低层模块,两者都应该依赖抽象。抽象不应该依赖细节,细节应该依赖抽象。
在DDD的语境下,领域层是高层模块(制定业务规则),基础设施层是低层模块(提供技术能力)。传统架构里高层直接依赖低层,DIP要求把这个方向反过来。
怎么反?在领域层定义一个抽象(PurchaseOrderRepository接口),让基础设施层去依赖这个抽象(PurchaseOrderRepositoryImpl实现这个接口)。两个模块都朝着抽象的方向看,而不是高层看着低层。
这是一个架构设计层面的决策。它发生在你画模块图、写pom.xml的时候,跟Spring容器没有任何关系。就算你的项目不用Spring,这个依赖方向的设计照样成立。
很多人会把这个现象归到IoC或DI头上,但准确地说,它是DIP。IoC和DI在这件事里扮演的是另一个角色。
DIP落地需要IoC和DI
DIP解决了编译期的依赖方向问题。领域模块不依赖基础设施模块,编译通过,模块间的隔离是干净的。
运行时还有一个问题:PurchaseOrderDomainServiceImpl需要一个PurchaseOrderRepository的实例,这个实例从哪来?
接口不能实例化,必须有人创建PurchaseOrderRepositoryImpl的对象,然后把它传给PurchaseOrderDomainServiceImpl。如果让PurchaseOrderDomainServiceImpl自己去new:
Java
// 如果这样写,领域层就必须import基础设施层的类,DIP白做了
private final PurchaseOrderRepository repo = new PurchaseOrderRepositoryImpl(...);
领域层的代码里出现了PurchaseOrderRepositoryImpl,编译时就需要依赖基础设施模块,DIP的设计前功尽弃。
这就是IoC和DI出场的地方。
IoC(控制反转) :PurchaseOrderDomainServiceImpl不负责创建自己的依赖对象,这件事交给Spring容器。容器在启动时扫描所有带@Repository、@Service注解的类,创建实例,管理它们的生命周期。
DI(依赖注入) :Spring容器发现PurchaseOrderDomainServiceImpl的构造器需要一个PurchaseOrderRepository类型的参数,同时容器里有一个PurchaseOrderRepositoryImpl实例实现了这个接口,于是把它注入进去。
整个过程发生在启动模块(erp-server)里。启动模块同时依赖领域模块和基础设施模块,Spring容器在这个模块的上下文中完成装配。领域模块的代码始终不知道PurchaseOrderRepositoryImpl的存在。
DIP在编译期切断了高层对低层的依赖,IoC和DI在运行时把它们重新连接起来。 三者配合,既保证了模块间的隔离,又让程序能正常运行。

DIP、IoC、DI的关系
这三个概念经常被放在一起讨论,但它们不在同一个层面上。
DIP是一条架构设计原则,告诉你模块间的依赖方向应该怎么安排。IoC是一种控制模式,把对象的创建和管理从业务代码移交给框架。DI是IoC最主流的实现方式,框架通过构造器或setter把依赖对象传给使用者。
三者的层次关系:DIP提出了问题(依赖方向不对),IoC给出了策略(让框架来管理),DI提供了具体手段(通过构造器注入)。
| 维度 | DIP(依赖反转原则) | IoC(控制反转) | DI(依赖注入) |
|---|---|---|---|
| 是什么 | 设计原则 | 控制模式 | 实现机制 |
| 解决什么问题 | 模块间的依赖方向不合理 | 业务代码不应自己创建依赖 | 怎么把依赖传给使用者 |
| 作用时机 | 架构设计阶段(编译期) | 运行时 | 运行时 |
| 没有Spring能不能做 | 能,这是纯架构设计 | 能,但需要手写工厂或服务定位器 | 能,但需要手动通过构造器传参 |
| 在DDD项目中的体现 | 领域层定义接口,基础设施层实现接口,pom依赖方向反转 | Spring容器接管所有Bean的创建和生命周期 | @RequiredArgsConstructor构造器注入仓储接口 |
关于IoC和DI的基础概念,我在之前的一篇文章里有更详细的讲解,包括IoC思想的起源(1983年好莱坞原则)、Martin Fowler为什么提出用DI来替代IoC这个模糊的词,感兴趣可以去翻。
小结
大多数人接触这三个概念的顺序是反的:先学DI(被@Autowired注解引进门),然后了解IoC(知道Spring容器在帮你管理对象),最后才有可能接触到DIP。但从理解深度来看,DIP才是这三个概念中真正影响架构质量的那个。IoC和DI是让DIP能够在Spring生态里落地的技术手段。
如果只把DI理解成「让框架帮你new对象」,确实看不出什么价值,不如自己写new来得直接。DI的价值要放在DIP的语境下才能看到:DI不是为了省一个new,而是为了让模块间的依赖方向可以不跟调用方向走。 调用方向是领域层调用基础设施层的能力,但依赖方向可以是基础设施层依赖领域层。这种能力对大型项目的架构稳定性影响很大。
我在团队里落地DDD这几年,发现对这三个概念理解最到位的开发者,往往不是背概念背出来的,而是在拆模块、定义接口、处理模块间依赖的实际过程中,自己体会到的。如果你也在做DDD或者类似的模块化设计,建议打开项目的pom.xml,看看模块间的依赖方向是不是符合DIP。这比读十篇概念解析的文章有效。
希望这篇内容可以帮到你。
最近在知乎出了秒杀专栏,感兴趣的可以订阅一下。至于知识星球的,可以搜:
老码头的技术浮生录
它是一个能实际帮你解决难题的星球。
我的知乎账号:
- SamDeepThinking