关于循环依赖的答疑
Overview
问题:能否举个循环依赖的例子
假设,某个单体应用模块如下:
1project
2├── bpm-module
3│ ├── bpm-module-api # 定义 BPM 的通用接口
4│ └── bpm-module-impl # BPM 的具体实现,依赖 bpm-module-api 和 biz-module-api
5├── biz-module
6│ ├── biz-module-api # 定义业务模块的通用接口
7│ └── biz-module-impl # 业务模块的具体实现,依赖 bpm-module-api 和 biz-module-api
依赖关系说明
-
bpm-module-api:
- 定义流程相关的接口(如
ProcessService
和回调接口BizCallbackInterface
)。 - 该模块不依赖任何其他模块。
- 定义流程相关的接口(如
-
bpm-module-impl:
- 实现
bpm-module-api
中定义的流程管理功能。 - 依赖 biz-module-api 来调用业务模块提供的回调接口。
- 实现
-
biz-module-api:
- 定义业务模块相关的接口(如
BusinessService
)。 - 该模块不依赖任何其他模块。
- 定义业务模块相关的接口(如
-
biz-module-impl:
- 实现
biz-module-api
中定义的业务逻辑功能。 - 依赖 bpm-module-api 来调用流程相关的功能。
- 实现
通过这种设计,两个模块的实现层互相依赖,但依赖的仅仅是对方的 API
层,而不是具体实现。
示例代码
bpm-module-api: 定义流程管理功能
1public interface ProcessService {
2 void createProcessInstance();
3}
4
5public interface BizCallbackInterface {
6 void onProcessCompleted(String processId);
7}
bpm-module-impl: 实现流程管理功能
1@Service
2public class ProcessServiceImpl implements ProcessService {
3 private final BizCallbackInterface bizCallback;
4
5 public ProcessServiceImpl(BizCallbackInterface bizCallback) {
6 this.bizCallback = bizCallback;
7 }
8
9 @Override
10 public void createProcessInstance() {
11 // 创建流程实例逻辑
12 String processId = "123";
13 // 回调业务模块
14 bizCallback.onProcessCompleted(processId);
15 }
16}
biz-module-api: 定义业务模块功能
1public interface BusinessService {
2 void startBusinessProcess();
3}
biz-module-impl: 实现业务逻辑
1@Service
2public class BusinessServiceImpl implements BusinessService {
3 private final ProcessService processService;
4
5 public BusinessServiceImpl(ProcessService processService) {
6 this.processService = processService;
7 }
8
9 @Override
10 public void startBusinessProcess() {
11 // 调用 BPM 模块的功能
12 processService.createProcessInstance();
13 }
14}
依赖配置示意
- bpm-module-impl 的
pom.xml
:
1<dependencies>
2 <dependency>
3 <groupId>com.example.project</groupId>
4 <artifactId>bpm-module-api</artifactId>
5 </dependency>
6 <dependency>
7 <groupId>com.example.project</groupId>
8 <artifactId>biz-module-api</artifactId>
9 </dependency>
10</dependencies>
- biz-module-impl 的
pom.xml
:
1<dependencies>
2 <dependency>
3 <groupId>com.example.project</groupId>
4 <artifactId>biz-module-api</artifactId>
5 </dependency>
6 <dependency>
7 <groupId>com.example.project</groupId>
8 <artifactId>bpm-module-api</artifactId>
9 </dependency>
10</dependencies>
优势
- 模块解耦:实现层只依赖 API 层,避免了直接的循环依赖。
- 职责分离:API 层只定义接口,逻辑集中在实现层。
- 可扩展性强:未来可以独立替换实现模块,而不会影响其他模块。
- 方便测试:API 层可以使用 mock 测试,避免测试时加载整个模块。
通过这种方式,你的双向依赖问题就转化为通过 API
层的单向依赖,设计更加清晰、合理。
问题:但是实际上,bpm-module-impl 依赖biz-module-api ,biz-module-impl依赖bpm-module-api,这不还是双向依赖吗?
从模块间的依赖路径来看,你的描述确实构成了逻辑上的双向依赖,但这里的双向依赖并不是直接的实现层依赖,而是通过 API 层 间接连接的。这样的设计实际上是可以接受的,因为:
-
解耦了实现细节:
bpm-module-impl
和biz-module-impl
并没有直接互相依赖,它们只依赖对方的 API 层,这使得每个模块的实现细节对对方透明。- API 层仅定义契约,不包含实现逻辑。
-
解决了循环依赖的技术问题:
- 编译时,
bpm-module-impl
和biz-module-impl
的依赖路径是清晰且无环的。 - 部署时,两个模块通过 Spring 的依赖注入(或其他 DI 框架)协作,避免了实例化循环。
- 编译时,
-
模块职责分离:
- API 层职责是定义接口和契约,实现层才是真正依赖链的终端。只要 API 不发生频繁变更,模块间的耦合是稳定的。
深入分析与改进策略
虽然这种设计在一定程度上是合理的,但仍然可能引入一定的复杂性,尤其是在以下情况下:
- 模块边界不清晰:模块职责定义模糊,可能导致过多的跨模块调用。
- 扩展性受限:将来如果希望拆分为微服务架构,双向依赖可能成为一个阻碍。
如果你希望进一步优化,可以考虑以下几种改进策略:
1. 通过事件解耦
使用事件驱动模型,消除模块间的直接调用关系。
-
场景:
bpm-module-impl
创建流程实例后,发布事件。biz-module-impl
监听事件并执行相应的业务逻辑。
-
优点:
- 模块间的直接依赖被削弱为松散耦合的事件关系。
- 便于扩展和演化为分布式系统。
-
示例代码:
bpm-module-api 定义事件:
1public class ProcessCreatedEvent {
2 private final String processId;
3
4 public ProcessCreatedEvent(String processId) {
5 this.processId = processId;
6 }
7
8 public String getProcessId() {
9 return processId;
10 }
11}
bpm-module-impl 发布事件:
1@Service
2public class ProcessServiceImpl implements ProcessService {
3 private final ApplicationEventPublisher eventPublisher;
4
5 public ProcessServiceImpl(ApplicationEventPublisher eventPublisher) {
6 this.eventPublisher = eventPublisher;
7 }
8
9 @Override
10 public void createProcessInstance() {
11 String processId = "123";
12 eventPublisher.publishEvent(new ProcessCreatedEvent(processId));
13 }
14}
biz-module-impl 监听事件:
1@Component
2public class ProcessEventListener {
3 @EventListener
4 public void handleProcessCreated(ProcessCreatedEvent event) {
5 System.out.println("Handling process created: " + event.getProcessId());
6 }
7}
2. 使用中间层进行协调
引入一个独立的 coordinator-module
,作为 bpm-module
和 biz-module
的调度者。
-
设计思路:
coordinator-module
依赖两个模块的 API。- 两个模块只与
coordinator-module
交互,避免直接调用对方的 API。
-
优点:
- 模块边界更加清晰,避免模块间的直接依赖。
- 中间层可以集中处理跨模块的逻辑。
-
项目结构:
1project
2├── bpm-module
3│ ├── bpm-module-api
4│ └── bpm-module-impl
5├── biz-module
6│ ├── biz-module-api
7│ └── biz-module-impl
8└── coordinator-module
3. 重构模块职责
如果模块间调用是频繁且高度耦合的,可以考虑重新划分模块边界:
- 将共同关注的功能合并到一个模块。
- 根据职责明确性将模块拆分为新的粒度。
总结
你当前的设计(bpm-module-impl
依赖 biz-module-api
,biz-module-impl
依赖 bpm-module-api
)在单体应用中是合理的,尤其是模块间通过 API 解耦时。不过,如果期望更高的灵活性和扩展性,建议探索事件驱动模型或中间协调模块的设计,以减少模块间的逻辑耦合。