Android 架构进阶:为什么项目越大,越需要把对象创建权拿走

1 阅读7分钟

刚开始写 Android 项目的时候,我其实不太理解 DI 有什么必要。

一个对象而已:

val repository = UserRepositoryImpl()

直接创建不就完了吗?

如果只是一个小项目,确实没什么问题。

甚至我觉得,这时候为了 HiltKoin 再引入一套依赖注入体系,反而有点重。

真正让我改变想法的,是项目开始变大以后。

你会发现,项目变复杂以后,麻烦来了:

到处都是创建对象。


一、项目小的时候,直接创建对象没有问题

比如:

class UserRepository {

    fun getUser() {
        // ...
    }
}

使用的时候:

class UserViewModel {

    private val repository = UserRepository()

    fun loadUser() {
        repository.getUser()
    }
}

完全没问题。

因为这个时候:

  • 依赖很少
  • 实现很明确
  • 生命周期简单
  • 对象可能只有一个地方使用

所以:

UserRepository()

本身不是问题。

真正的问题,是随着项目变大以后,new 开始出现在越来越多的地方。


二、真正麻烦的是:业务代码开始负责“组装对象”

假设登录功能一开始很简单:

class LoginServiceImpl(
    private val repository: UserRepository
)

创建:

val service = LoginServiceImpl(repository)

没什么问题。

后来需求慢慢增加。

登录需要配置:

class LoginConfig

需要安全组件:

class SecurityManager

最后可能变成:

class LoginServiceImpl(
    private val repository: UserRepository,
    private val config: LoginConfig,
    private val logger: Logger,
    private val securityManager: SecurityManager
)

于是创建它的人也开始变复杂:

val service = LoginServiceImpl(
    repository,
    config,
    logger,
    securityManager
)

这时候真正的问题出现了。

谁在创建 LoginServiceImpl,谁就必须知道它依赖什么。

也就是说:

LoginViewModel
      ↓
知道 LoginServiceImpl
      ↓
知道它需要 Repository
      ↓
知道它需要 Config
      ↓
知道它需要 SecurityManager

业务代码开始了解越来越多的基础设施。

这才是麻烦的开始。


三、对象创建本身不复杂,复杂的是依赖关系

很多人第一次接触 DI,会把它理解成:

Hilt 帮我创建对象。

这个理解太浅了。

因为创建一个对象真的很简单:

UserRepositoryImpl()

真正复杂的是:

UserViewModel
      ↓
LoginUseCase
      ↓
LoginRepository
      ↓
UserApi
      ↓
Retrofit
      ↓
OkHttp

这已经不是“创建一个对象”了。

这是一整张对象依赖图

而项目继续变大以后,这张图可能变成:

                    Retrofit
                       ↓
                    UserApi
                       ↓
                UserRepository
                  ↙        ↘
             Database      Cache
                  ↓          ↓
                 Room      DataStore


UserRepository
       ↓
   LoginUseCase
       ↓
  LoginViewModel
       ↓
       UI

真正难管理的是这张图。


四、如果每个业务类都可以创建对象,依赖关系迟早会失控

比如:

class LoginViewModel {

    private val api =
        Retrofit.Builder()
            .baseUrl("...")
            .build()
            .create(UserApi::class.java)
}

一开始你可能觉得:

能跑就行。

但这样一来,LoginViewModel 已经知道了:

  • Retrofit
  • BaseUrl
  • UserApi
  • 网络配置

ViewModel 本来应该关心:

用户点击登录
        ↓
执行登录
        ↓
展示结果

现在却开始关心:

Retrofit 怎么初始化
API 怎么创建
BaseUrl 是什么

业务代码和基础设施绑在了一起。

以后你想把 Retrofit 换掉,就会发现:

不是不能改,而是改起来很别扭。


五、DI 真正做的事情,是把“决定权”拿走

所以 DI 真正变化的是:

LoginViewModel 不再决定 LoginService 是谁。

它只声明:

class LoginViewModel @Inject constructor(
    private val loginService: LoginService
)

ViewModel 只关心一件事情:

我需要一个 LoginService

至于:

到底是 LoginServiceImpl?

还是 CacheLoginServiceImpl?

还是 FakeLoginService?

它需要哪些参数?

这些参数从哪里来?

生命周期是什么?

这些事情不再由 ViewModel 决定。


六、这就是 IoC:控制权发生了变化

没有 DI 的时候:

业务代码
   ↓
我要 LoginService
   ↓
我自己创建
   ↓
LoginServiceImpl
   ↓
我自己解决它的依赖

业务代码拥有控制权。

而有 DI 以后:

业务代码
   ↓
声明:
我需要 LoginService
   ↓
DI
   ↓
决定使用哪个实现
   ↓
解决它的依赖
   ↓
创建对象
   ↓
注入业务代码

所以 IoC(Inversion of Control)真正“反转”的,并不是:

创建对象的方法。

而是:

谁拥有依赖关系的控制权。

以前是业务代码决定。

现在交给架构层决定。


七、项目越大,这件事情就越重要

因为项目越大,依赖关系越复杂。

小项目可能只有:

ViewModel
   ↓
Repository

直接创建完全没问题。

中型项目可能已经变成:

ViewModel
   ↓
UseCase
   ↓
Repository
   ↓
Api
Database
Cache
Config
Logger

大型项目可能还要考虑:

不同实现
不同环境
不同模块
不同生命周期
不同配置

这时候如果每个业务类都可以决定:

我要创建哪个对象。

最后整个项目会出现一个很麻烦的情况:

依赖关系散落在业务代码的各个角落。

你根本不知道一个对象到底在哪里被创建。


八、真正值得关注的是“实现类变化”

比如现在:

interface LoginService {

    fun login()
}

生产环境:

class LoginServiceImpl : LoginService

测试环境:

class FakeLoginService : LoginService

以后因为业务需求,又增加了缓存:

class CacheLoginService : LoginService

如果业务代码直接依赖实现:

val service = LoginServiceImpl()

那实现发生变化的时候,业务代码也要跟着变化。

但如果业务代码只依赖接口:

class LoginViewModel(
    private val service: LoginService
)

那么:

LoginViewModel
       ↓
LoginService
       ↑
       |
 ┌─────┼────────────┐
 ↓     ↓            ↓
Impl  Fake       CacheImpl

实现怎么变化,业务代码都不需要知道。

这才是 DI 和抽象真正结合起来之后的价值。


九、构造参数变化,也是一个很现实的问题

比如最开始:

class LoginServiceImpl(
    private val repository: UserRepository
)

创建:

LoginServiceImpl(repository)

后来:

class LoginServiceImpl(
    private val repository: UserRepository,
    private val config: LoginConfig
)

再后来:

class LoginServiceImpl(
    private val repository: UserRepository,
    private val config: LoginConfig,
    private val logger: Logger
)

如果这个对象在很多地方直接创建:

LoginServiceImpl(...)

构造函数变化以后,所有创建点都可能受到影响。

而 DI 的意义就在这里。

让:

LoginServiceImpl

的创建集中在依赖配置的位置。

例如 Hilt:

@Module
@InstallIn(SingletonComponent::class)
object LoginModule {

    @Provides
    fun provideLoginService(
        repository: UserRepository,
        config: LoginConfig,
        logger: Logger
    ): LoginService {
        return LoginServiceImpl(
            repository,
            config,
            logger
        )
    }
}

业务代码不需要跟着构造参数变化。

它依然只是:

class LoginViewModel @Inject constructor(
    private val loginService: LoginService
)

十、所以 DI 管理的,其实是一张对象图

这也是我现在觉得理解 DI 最重要的一步。

不要只把 DI 理解成:

@Inject
    ↓
自动创建对象

应该把它理解成:

                 UserApi
                    ↑
                    |
              UserRepository
                    ↑
                    |
                LoginUseCase
                    ↑
                    |
              LoginViewModel

DI 做的事情,是把这些依赖关系连接起来。

也就是:

谁依赖谁
      ↓
谁实现谁
      ↓
谁负责创建
      ↓
谁负责管理生命周期
      ↓
不同环境使用什么实现

这些事情被集中管理以后,业务代码就可以干净很多。


十、什么时候应该开始考虑 DI?

我觉得可以看一个很简单的信号:

当一个业务类开始越来越关心“它的依赖怎么创建”时,就该考虑把创建权拿走了。

比如:

class UserViewModel {

    private val retrofit = ...
    private val api = ...
    private val database = ...
    private val repository = ...
}

如果一个 ViewModel 变成这样:

既负责业务
又负责创建 Retrofit
又负责创建 Repository
又负责管理配置
又负责决定生命周期

那问题已经不是代码多了。

而是:

职责已经开始混在一起了。

这时候 DI 就有意义了。


最后、再回到最开始的问题

DI 是不是帮我们创建对象?

是。

但这只是最表面的一层。

真正值得理解的是:

小项目

UserRepository()
        ↓
直接创建
        ↓
没什么问题

随着项目变大:

UserRepository
       ↓
Api
Database
Cache
Config
Logger
       ↓
依赖越来越多
       ↓
对象创建越来越复杂
       ↓
业务代码开始关心基础设施

这时候就需要把创建权拿走:

业务代码
    ↓
只声明依赖
    ↓
接口 / 抽象
    ↓
DI
    ↓
决定实现
    ↓
解决依赖
    ↓
管理生命周期

所以我现在更愿意这样理解 DI:

DI 不是为了让你少写几行 new

它真正解决的是:当系统越来越复杂以后,谁来负责决定对象之间的依赖关系。

小项目里,自己创建对象没什么。

但当一个项目里开始出现几十、几百个对象,以及复杂的依赖关系时,如果每个业务类都拥有“创建依赖”的权力,系统很容易慢慢失控。

所以项目越大,越应该把这部分权力从业务代码里拿出来。

业务代码负责“我要什么”。

架构负责“给你什么”。 有了DI,我们可以专注业务开发了。

Github Sample