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