Clean Architecture 中 UI 层怎么调用 UseCase 才不破坏分层?

程序猿慧慧 阅读 12

我最近在用 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>

虽然能跑,但总觉得不对劲——是不是应该通过某种依赖注入或者接口抽象来解耦?求指点!

我来解答 赞 2 收藏
二维码
手机扫码查看
1 条解答
上官玉惠
你这个问题问得很到位,很多刚接触 Clean Architecture 的人都会卡在这里,觉得“我明明按分层写的,怎么一到用起来就绕回来了”——其实不是你理解错了,而是很多人只讲了分层结构,没讲清楚实际调用时怎么把依赖倒置起来用。

原理是这样:Clean Architecture 里的“依赖规则”说的是源码层面的依赖方向,不是运行时的调用方式。也就是说,外层代码(比如 UI)可以“调用”内层的接口,但不能在源码里“导入”内层的具体实现类,否则就形成了反向依赖,破坏了分层。

你现在的写法:

import { GetUserUseCase } from '@/core/usecases/GetUserUseCase';
import { UserRepository } from '@/data/repositories/UserRepository';

const userRepo = new UserRepository();
const getUserUseCase = new GetUserUseCase(userRepo);


问题出在 new UserRepository() 这里——UI 层直接 new 了一个 data 层的具体实现类,这相当于在源码里 import 了 data 层,虽然运行时能跑,但源码依赖方向是错的:presentation → data,而 Clean Architecture 要求的是 data → presentation(通过接口)。

正确做法是:让 UI 层只依赖 UseCase 的接口,而 UseCase 的依赖(Repository)由外部注入。这里的关键是“依赖注入 + 依赖倒置”。

具体分三步来:

第一步:在 domain 层定义接口(Repository 的接口),比如 userRepository 接口应该在 domain 层:

// domain/repositories/userRepository.js
export class UserRepository {
// 只定义抽象方法,不实现
async getUserById(id) {
throw new Error('Not implemented');
}
}


第二步:在 data 层实现这个接口,实现细节(比如调用 API、数据库):

// data/repositories/userRepository.js
import { UserRepository } from '@/domain/repositories/userRepository';

export class UserDataRepository extends UserRepository {
async getUserById(id) {
// 实际请求逻辑,比如 fetch
const res = await fetch(/api/users/${id});
return res.json();
}
}


第三步:在 usecase 层只依赖接口,不依赖实现:

// core/usecases/GetUserUseCase.js
export class GetUserUseCase {
constructor(userRepository) {
this.userRepository = userRepository;
}

async execute(id) {
return await this.userRepository.getUserById(id);
}
}


到这里结构还是对的,关键在 UI 层怎么用。UI 层不能 new UserRepository,但可以接收一个已实例化的 UseCase。所以你需要一个“组装层”——通常叫 Factory、DI Container 或者简单的 Injector。

最简单的做法是在应用启动时,把所有依赖拼好,然后传给 UI 组件(比如通过 props 或 provide/inject)。比如在 Vue 里你可以这样:

// main.js 或 app.js(应用启动时)
import { UserDataRepository } from '@/data/repositories/userRepository';
import { GetUserUseCase } from '@/core/usecases/GetUserUseCase';

const userRepository = new UserDataRepository();
const getUserUseCase = new GetUserUseCase(userRepository);

// 通过 provide 注入,或者挂到全局(不推荐全局变量,但简单项目可以)
app.provide('getUserUseCase', getUserUseCase);


然后在组件里这样用:

// UserView.vue
<script setup>
import { inject } from 'vue';

// 从父级注入 UseCase 实例,而不是自己 new
const getUserUseCase = inject('getUserUseCase');

const loadUser = async () => {
const user = await getUserUseCase.execute('123');
console.log(user);
};
</script>


这样 UI 层就只依赖了 UseCase 的接口(也就是它本身),而 UseCase 的具体实现(Repository)是由外部注入的,源码依赖方向就对了:

- UI(presentation)依赖 UseCase(core)
- UseCase(core)依赖 UserRepository(domain)
- UserRepository(domain)不依赖 data
- UserDataRepository(data)实现 UserRepository(domain)

这就是依赖倒置:高层模块(UI)和低层模块(data)都依赖抽象(domain 层接口),而不是低层直接被高层依赖。

再补充一点:如果你项目小,不想搞 DI Container,也可以用一个简单的“工厂函数”封装组装逻辑:

// core/factories/useCasesFactory.js
import { UserDataRepository } from '@/data/repositories/userRepository';
import { GetUserUseCase } from '@/core/usecases/GetUserUseCase';

export function createGetUserUseCase() {
const userRepository = new UserDataRepository();
return new GetUserUseCase(userRepository);
}


然后 UI 层:

<script setup>
import { createGetUserUseCase } from '@/core/factories/useCasesFactory';

const getUserUseCase = createGetUserUseCase();
const user = await getUserUseCase.execute('123');
</script>


这样也行,因为 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 的。
点赞 3
2026-02-24 22:00