刚开始写 Android 项目的时候,我其实不太理解 DI 有什么必要。
一个对象而已:
1val repository = UserRepositoryImpl() 2
直接创建不就完了吗?
如果只是一个小项目,确实没什么问题。
甚至我觉得,这时候为了 Hilt、Koin 再引入一套依赖注入体系,反而有点重。
真正让我改变想法的,是项目开始变大以后。
你会发现,项目变复杂以后,麻烦来了:
到处都是创建对象。
一、项目小的时候,直接创建对象没有问题
比如:
1class UserRepository { 2 3 fun getUser() { 4 // ... 5 } 6} 7
使用的时候:
1class UserViewModel { 2 3 private val repository = UserRepository() 4 5 fun loadUser() { 6 repository.getUser() 7 } 8} 9
完全没问题。
因为这个时候:
- 依赖很少
- 实现很明确
- 生命周期简单
- 对象可能只有一个地方使用
所以:
1UserRepository() 2
本身不是问题。
真正的问题,是随着项目变大以后,new 开始出现在越来越多的地方。
二、真正麻烦的是:业务代码开始负责“组装对象”
假设登录功能一开始很简单:
1class LoginServiceImpl( 2 private val repository: UserRepository 3) 4
创建:
1val service = LoginServiceImpl(repository) 2
没什么问题。
后来需求慢慢增加。
登录需要配置:
1class LoginConfig 2
需要安全组件:
1class SecurityManager 2
最后可能变成:
1class LoginServiceImpl( 2 private val repository: UserRepository, 3 private val config: LoginConfig, 4 private val logger: Logger, 5 private val securityManager: SecurityManager 6) 7
于是创建它的人也开始变复杂:
1val service = LoginServiceImpl( 2 repository, 3 config, 4 logger, 5 securityManager 6) 7
这时候真正的问题出现了。
谁在创建 LoginServiceImpl,谁就必须知道它依赖什么。
也就是说:
1LoginViewModel 2 ↓ 3知道 LoginServiceImpl 4 ↓ 5知道它需要 Repository 6 ↓ 7知道它需要 Config 8 ↓ 9知道它需要 SecurityManager 10
业务代码开始了解越来越多的基础设施。
这才是麻烦的开始。
三、对象创建本身不复杂,复杂的是依赖关系
很多人第一次接触 DI,会把它理解成:
Hilt 帮我创建对象。
这个理解太浅了。
因为创建一个对象真的很简单:
1UserRepositoryImpl() 2
真正复杂的是:
1UserViewModel 2 ↓ 3LoginUseCase 4 ↓ 5LoginRepository 6 ↓ 7UserApi 8 ↓ 9Retrofit 10 ↓ 11OkHttp 12
这已经不是“创建一个对象”了。
这是一整张对象依赖图。
而项目继续变大以后,这张图可能变成:
1 Retrofit 2 ↓ 3 UserApi 4 ↓ 5 UserRepository 6 ↙ ↘ 7 Database Cache 8 ↓ ↓ 9 Room DataStore 10 11 12UserRepository 13 ↓ 14 LoginUseCase 15 ↓ 16 LoginViewModel 17 ↓ 18 UI 19
真正难管理的是这张图。
四、如果每个业务类都可以创建对象,依赖关系迟早会失控
比如:
1class LoginViewModel { 2 3 private val api = 4 Retrofit.Builder() 5 .baseUrl("...") 6 .build() 7 .create(UserApi::class.java) 8} 9
一开始你可能觉得:
能跑就行。
但这样一来,LoginViewModel 已经知道了:
- Retrofit
- BaseUrl
- UserApi
- 网络配置
ViewModel 本来应该关心:
1用户点击登录 2 ↓ 3执行登录 4 ↓ 5展示结果 6
现在却开始关心:
1Retrofit 怎么初始化 2API 怎么创建 3BaseUrl 是什么 4
业务代码和基础设施绑在了一起。
以后你想把 Retrofit 换掉,就会发现:
不是不能改,而是改起来很别扭。
五、DI 真正做的事情,是把“决定权”拿走
所以 DI 真正变化的是:
LoginViewModel 不再决定 LoginService 是谁。
它只声明:
1class LoginViewModel @Inject constructor( 2 private val loginService: LoginService 3) 4
ViewModel 只关心一件事情:
我需要一个
LoginService。
至于:
1到底是 LoginServiceImpl? 2 3还是 CacheLoginServiceImpl? 4 5还是 FakeLoginService? 6 7它需要哪些参数? 8 9这些参数从哪里来? 10 11生命周期是什么? 12 13
这些事情不再由 ViewModel 决定。
六、这就是 IoC:控制权发生了变化
没有 DI 的时候:
1业务代码 2 ↓ 3我要 LoginService 4 ↓ 5我自己创建 6 ↓ 7LoginServiceImpl 8 ↓ 9我自己解决它的依赖 10
业务代码拥有控制权。
而有 DI 以后:
1业务代码 2 ↓ 3声明: 4我需要 LoginService 5 ↓ 6DI 7 ↓ 8决定使用哪个实现 9 ↓ 10解决它的依赖 11 ↓ 12创建对象 13 ↓ 14注入业务代码 15
所以 IoC(Inversion of Control)真正“反转”的,并不是:
创建对象的方法。
而是:
谁拥有依赖关系的控制权。
以前是业务代码决定。
现在交给架构层决定。
七、项目越大,这件事情就越重要
因为项目越大,依赖关系越复杂。
小项目可能只有:
1ViewModel 2 ↓ 3Repository 4
直接创建完全没问题。
中型项目可能已经变成:
1ViewModel 2 ↓ 3UseCase 4 ↓ 5Repository 6 ↓ 7Api 8Database 9Cache 10Config 11Logger 12
大型项目可能还要考虑:
1不同实现 2不同环境 3不同模块 4不同生命周期 5不同配置 6
这时候如果每个业务类都可以决定:
我要创建哪个对象。
最后整个项目会出现一个很麻烦的情况:
依赖关系散落在业务代码的各个角落。
你根本不知道一个对象到底在哪里被创建。
八、真正值得关注的是“实现类变化”
比如现在:
1interface LoginService { 2 3 fun login() 4} 5
生产环境:
1class LoginServiceImpl : LoginService 2
测试环境:
1class FakeLoginService : LoginService 2
以后因为业务需求,又增加了缓存:
1class CacheLoginService : LoginService 2
如果业务代码直接依赖实现:
1val service = LoginServiceImpl() 2
那实现发生变化的时候,业务代码也要跟着变化。
但如果业务代码只依赖接口:
1class LoginViewModel( 2 private val service: LoginService 3) 4
那么:
1LoginViewModel 2 ↓ 3LoginService 4 ↑ 5 | 6 ┌─────┼────────────┐ 7 ↓ ↓ ↓ 8Impl Fake CacheImpl 9
实现怎么变化,业务代码都不需要知道。
这才是 DI 和抽象真正结合起来之后的价值。
九、构造参数变化,也是一个很现实的问题
比如最开始:
1class LoginServiceImpl( 2 private val repository: UserRepository 3) 4
创建:
1LoginServiceImpl(repository) 2
后来:
1class LoginServiceImpl( 2 private val repository: UserRepository, 3 private val config: LoginConfig 4) 5
再后来:
1class LoginServiceImpl( 2 private val repository: UserRepository, 3 private val config: LoginConfig, 4 private val logger: Logger 5) 6
如果这个对象在很多地方直接创建:
1LoginServiceImpl(...) 2
构造函数变化以后,所有创建点都可能受到影响。
而 DI 的意义就在这里。
让:
1LoginServiceImpl 2
的创建集中在依赖配置的位置。
例如 Hilt:
1@Module 2@InstallIn(SingletonComponent::class) 3object LoginModule { 4 5 @Provides 6 fun provideLoginService( 7 repository: UserRepository, 8 config: LoginConfig, 9 logger: Logger 10 ): LoginService { 11 return LoginServiceImpl( 12 repository, 13 config, 14 logger 15 ) 16 } 17} 18
业务代码不需要跟着构造参数变化。
它依然只是:
1class LoginViewModel @Inject constructor( 2 private val loginService: LoginService 3) 4
十、所以 DI 管理的,其实是一张对象图
这也是我现在觉得理解 DI 最重要的一步。
不要只把 DI 理解成:
1@Inject 2 ↓ 3自动创建对象 4
应该把它理解成:
1 UserApi 2 ↑ 3 | 4 UserRepository 5 ↑ 6 | 7 LoginUseCase 8 ↑ 9 | 10 LoginViewModel 11
DI 做的事情,是把这些依赖关系连接起来。
也就是:
1谁依赖谁 2 ↓ 3谁实现谁 4 ↓ 5谁负责创建 6 ↓ 7谁负责管理生命周期 8 ↓ 9不同环境使用什么实现 10
这些事情被集中管理以后,业务代码就可以干净很多。
十、什么时候应该开始考虑 DI?
我觉得可以看一个很简单的信号:
当一个业务类开始越来越关心“它的依赖怎么创建”时,就该考虑把创建权拿走了。
比如:
1class UserViewModel { 2 3 private val retrofit = ... 4 private val api = ... 5 private val database = ... 6 private val repository = ... 7} 8
如果一个 ViewModel 变成这样:
1既负责业务 2又负责创建 Retrofit 3又负责创建 Repository 4又负责管理配置 5又负责决定生命周期 6
那问题已经不是代码多了。
而是:
职责已经开始混在一起了。
这时候 DI 就有意义了。
最后、再回到最开始的问题
DI 是不是帮我们创建对象?
是。
但这只是最表面的一层。
真正值得理解的是:
1小项目 2 3UserRepository() 4 ↓ 5直接创建 6 ↓ 7没什么问题 8
随着项目变大:
1UserRepository 2 ↓ 3Api 4Database 5Cache 6Config 7Logger 8 ↓ 9依赖越来越多 10 ↓ 11对象创建越来越复杂 12 ↓ 13业务代码开始关心基础设施 14
这时候就需要把创建权拿走:
1业务代码 2 ↓ 3只声明依赖 4 ↓ 5接口 / 抽象 6 ↓ 7DI 8 ↓ 9决定实现 10 ↓ 11解决依赖 12 ↓ 13管理生命周期 14
所以我现在更愿意这样理解 DI:
DI 不是为了让你少写几行
new。它真正解决的是:当系统越来越复杂以后,谁来负责决定对象之间的依赖关系。
小项目里,自己创建对象没什么。
但当一个项目里开始出现几十、几百个对象,以及复杂的依赖关系时,如果每个业务类都拥有“创建依赖”的权力,系统很容易慢慢失控。
所以项目越大,越应该把这部分权力从业务代码里拿出来。
业务代码负责“我要什么”。
**架构负责“给你什么”。**有了DI,我们可以专注业务开发了。
《Android 架构进阶:为什么项目越大,越需要把对象创建权拿走》 是转载文章,点击查看原文。
