如何按Clean Architecture要求隔离Dagger2,实现业务逻辑无感知?
完全理解你的困惑——我当初在把Dagger2和Clean Architecture结合的时候也纠结过这个点!Robert Martin强调的「DI框架是实现细节,要远离业务核心层」确实是能落地的,核心思路就是让业务层只依赖抽象的注入接口,把Dagger的实现完全封装在框架层,具体可以这么做:
1. 定义抽象的注入接口(业务层唯一依赖的DI相关类)
首先,在靠近框架层的地方创建一个纯抽象的注入接口,里面只暴露业务层需要的依赖获取方法,完全不涉及任何Dagger的注解或类。业务层只会和这个接口打交道,根本不知道背后是Dagger在工作。
比如:
// 这个接口属于框架层的抽象,业务层可以安全依赖它 interface AppInjector { // 暴露业务层需要的用例、仓库等依赖 fun getLoginUseCase(): LoginUseCase fun getUserRepository(): UserRepository }
2. 让Dagger Component实现这个抽象接口
接下来,让你的Dagger Component去实现上面的抽象接口,把Dagger的所有实现细节都封装在这里。业务层看不到这个Component的存在,它只在主模块(比如Application)里被初始化。
举个例子:
// 这个类完全属于框架层,只有主模块会引用它 @Singleton @Component(modules = [RepositoryModule::class, UseCaseModule::class]) interface AppComponent : AppInjector { @Component.Factory interface Factory { fun create(@BindsInstance context: Context): AppComponent } }
对应的Module也只负责提供具体实现,同样属于框架层:
@Module class RepositoryModule { @Provides @Singleton fun provideUserRepository(): UserRepository { return UserRepositoryImpl() } } @Module class UseCaseModule { @Provides fun provideLoginUseCase(repo: UserRepository): LoginUseCase { return LoginUseCase(repo) } }
3. 在主模块初始化Dagger,传递抽象接口实例
在你的Application类(主模块)里,初始化Dagger Component,并把它赋值给抽象的AppInjector类型的变量。这样上层代码拿到的永远是抽象接口,而非具体的Dagger实现。
class MyApp : Application() { lateinit var injector: AppInjector override fun onCreate() { super.onCreate() // 这里是唯一直接引用Dagger的地方 injector = DaggerAppComponent.factory().create(this) } }
4. 业务层完全脱离Dagger,只依赖抽象和构造注入
最关键的一步:你的实体、用例、抽象仓库等业务核心类,绝对不能出现任何Dagger注解(比如@Inject)或引用Dagger相关类。所有依赖都通过构造函数传入,依赖的实例由抽象的AppInjector提供。
比如用例类:
// 纯业务逻辑类,没有任何Dagger痕迹 class LoginUseCase(private val userRepository: UserRepository) { fun execute(username: String, password: String): Result<User> { // 这里只处理业务逻辑,完全不关心依赖从哪来 return userRepository.login(username, password) } }
在需要使用用例的地方(比如UI层),只通过抽象的AppInjector获取:
class LoginViewModel : ViewModel() { // 只依赖抽象的AppInjector,不知道Dagger的存在 private val loginUseCase = (MyApp.instance.injector).getLoginUseCase() fun login(username: String, password: String) { val result = loginUseCase.execute(username, password) // 处理登录结果 } }
额外的好处
这么做除了符合Clean Architecture的原则,还有两个实用优势:
- 替换DI框架成本极低:如果哪天想换成Hilt、Koin甚至手动DI,只需要重新实现
AppInjector接口,业务层代码一行都不用改。 - 测试更简单:写单元测试时,你可以快速实现一个Mock版的
AppInjector,提供Mock的依赖,完全不需要启动Dagger的注入流程。
内容的提问来源于stack exchange,提问作者1048576

