Clean Architecture 中 UI 层怎么调用 UseCase 才不破坏分层?
我最近在用 Clean Architecture 搭一个 Vue 项目,把逻辑拆成了 domain、usecase、data 和 presentation 几层。但我在组件里直接 import UseCase 的时候,感觉好像绕过了依赖规则,会不会破坏了“外层不能直接依赖内层”的原则?
比如我现在这样写:
<script setup>
import { GetUserUseCase } from '@/core/usecases/GetUserUseCase';
import { UserRepository } from '@/data/repositories/UserRepository';
const userRepo = new UserRepository();
const getUserUseCase = new GetUserUseCase(userRepo);
const user = await getUserUseCase.execute('123');
</script>
虽然能跑,但总觉得不对劲——是不是应该通过某种依赖注入或者接口抽象来解耦?求指点!
原理是这样:Clean Architecture 里的“依赖规则”说的是源码层面的依赖方向,不是运行时的调用方式。也就是说,外层代码(比如 UI)可以“调用”内层的接口,但不能在源码里“导入”内层的具体实现类,否则就形成了反向依赖,破坏了分层。
你现在的写法:
问题出在
new UserRepository()这里——UI 层直接 new 了一个 data 层的具体实现类,这相当于在源码里 import 了 data 层,虽然运行时能跑,但源码依赖方向是错的:presentation → data,而 Clean Architecture 要求的是 data → presentation(通过接口)。正确做法是:让 UI 层只依赖 UseCase 的接口,而 UseCase 的依赖(Repository)由外部注入。这里的关键是“依赖注入 + 依赖倒置”。
具体分三步来:
第一步:在 domain 层定义接口(Repository 的接口),比如 userRepository 接口应该在 domain 层:
第二步:在 data 层实现这个接口,实现细节(比如调用 API、数据库):
第三步:在 usecase 层只依赖接口,不依赖实现:
到这里结构还是对的,关键在 UI 层怎么用。UI 层不能 new UserRepository,但可以接收一个已实例化的 UseCase。所以你需要一个“组装层”——通常叫 Factory、DI Container 或者简单的 Injector。
最简单的做法是在应用启动时,把所有依赖拼好,然后传给 UI 组件(比如通过 props 或 provide/inject)。比如在 Vue 里你可以这样:
然后在组件里这样用:
这样 UI 层就只依赖了 UseCase 的接口(也就是它本身),而 UseCase 的具体实现(Repository)是由外部注入的,源码依赖方向就对了:
- UI(presentation)依赖 UseCase(core)
- UseCase(core)依赖 UserRepository(domain)
- UserRepository(domain)不依赖 data
- UserDataRepository(data)实现 UserRepository(domain)
这就是依赖倒置:高层模块(UI)和低层模块(data)都依赖抽象(domain 层接口),而不是低层直接被高层依赖。
再补充一点:如果你项目小,不想搞 DI Container,也可以用一个简单的“工厂函数”封装组装逻辑:
然后 UI 层:
这样也行,因为
createGetUserUseCase是 core 层的代码(它 import 了 data 层),而 UI 层只是调用 core 层的工厂函数——UI 层没直接 import data 层,所以源码依赖方向没被破坏。总结一下:
- domain 层放接口(Repository、UseCase 的抽象)
- data 层实现 domain 层的接口
- usecase 层只依赖 domain 层的接口
- UI 层只依赖 usecase 层(通过注入或工厂函数获取实例)
- 组装逻辑放在 core 层的 factory 或启动时初始化
这样分下来,你既能保持 Clean Architecture 的分层结构,又能写得自然不别扭。
我之前也踩过这个坑,以为“依赖注入”必须搞得很复杂,其实 Vue 里用 provide/inject 或者简单工厂函数就能搞定,关键是别让 UI 层自己 new 出具体实现类——那是 data 层的活,不是 UI 的。