依赖注入(零)—— 依赖查找与依赖注入

273 阅读5分钟

什么是依赖?

一个系统是由多个组件所构成的,这里的组件是广义上的组件,它可能是一个函数、一个类、一个模块、一个微服务。组件用于提供一些功能,这些功能可能会被其它组件所使用,像这种一个组件需要使用别的组件提供功能的情况,就是两个组件之间产生了依赖关系。其中提供功能的组件被称为依赖项。

比如在面向对象的程序开发中,我们最常接触到的组件是类,当一个类A需要调用其它类B的方法(功能)时,就说明类A和类B就有了依赖,称为类A依赖类B,类B是依赖项。

依赖关系

提供依赖项的三种方式

  1. 类中直接构造所需的依赖项。
  2. 从其它地方抓取(代表是依赖查找DL)。
  3. 通过参数注入(也就是依赖注入DI)。

在类中直接构造依赖项有什么问题?

class Car {
    private val engine = Engine()

    fun start() {
        engine.start()
    }
}

试想一下,在上面这个例子中,如果被依赖项Engine的创建过程非常复杂,那么Car中也就需要包含这样一份复杂创建过程的副本,其它需要用到Engine的类也会如此。重复往往意味着修改困难:当我们需要对engine的创建过程进行修改,就需要从茫茫的代码海中找到它,一个一个的修改过来。也不利于代码复用:当我们需要一个使用其它引擎的Car时,就不得不照着原有Car类创建新的Car

直接在类中构造依赖项还会让我们难以为依赖者编写测试,比如我们只想测试Car的功能是否正确,却不得不保证Engine能够正确运行(单元测试需要将依赖项与被测试对象隔离起来)。

依赖注入带来的问题

依赖注入也会带来一些问题:使用依赖注入构建的程序,其下层模块会将创建依赖的任务传递给上层模块,那么必然会导致最上层的模块最终需要创建所有必须的依赖,而到依赖的传递过程也会多很多setter或者增加很多构造参数。

DL(Dependency Lookup) - 依赖查找

别称:服务定位器(ServiceLocator)、对象工厂(Object Factory)、组件代理(Component Broker)、组件注册表(Component Registry)

依赖查找会让依赖者在需要的时候通过一个统一的全局定位器来获取对象,获取对象时通常需要传递Key、类型、路径等信息用于定位对象。

class Car {
    // 传递了类型信息以定位对象
    private val engine = ServiceLocator.get<Engine>()
    fun start() {
        engine.start()
    }
}

fun main() {
    ServiceLocator.register(Engine())
    Car().start()
}

上面的代码使用了服务定位器模式,这是依赖查找的一种实现,它将依赖都集中到了一起,通过同一个全局服务定位器为其它模块提供依赖。Android API中getSystemService()就是这种实现。

依赖查找可以帮我们解决前面所提到的直接构造依赖项的一些问题:依赖查找将依赖项的创建从依赖者中抽离出来,并将这些依赖项统一的管理起来,从而解决了直接构造依赖项中修改困难和复用困难两大问题。

但是,依赖查找也还是存在一些问题:

  • 难以测试:依赖查找使用统一的全局服务定位器提供依赖,使得所有的依赖者都必须与服务定位器进行交互,这使得代码更难测试。
  • 缺少编译时检查:依赖查找仍然是依赖者主动控制并请求注入对象,从外部无法了解到类需要什么以及需要的依赖项在服务定位器中是否存在,而这些错误只有到运行时才能发现。
  • 麻烦的作用域限定:依赖查找不太好管理除应用级生命周期外其它类型的生命周期的依赖。

在Android中基于依赖查找而实现的三方库有Koin、Kodein。

DI(Dependency Injection) - 依赖注入

依赖注入的概念其实很简单,就是将一个类所需要的依赖项,在外界构造好后,通过某种方式(构造参数、setter等)传递进去。

class Car(
    // 通过构造参数注入
    private val engine: Engine
) {
    // 通过字段/setter注入
    latinit var wheel: Wheel
    fun start() {
        engine.start()
    }
}

fun main() {
    val engine = Engine()
    val car = Car(engine)
    car.wheel = WheelFactory.create()
    car.start()
}

依赖注入基于控制反转(IoC - Inversion of Control)原则,它反转了依赖项的创建和获取过程:在之前直接构造依赖项中,依赖项的创建和获取是依赖者主动控制的,使用了依赖注入以后,依赖者则变为被动接收由其它地方创建好依赖项。

💡 前文依赖查找也是控制反转的一种实现,相比依赖注入,控制反转在依赖项的获取上要稍微主动一点。

依赖注入在解除依赖者与依赖项之间的强耦合的同时,还通过接口对外暴露了所需要的依赖项,这使得依赖注入相比依赖查找更容易测试(你可以通过注入Mock对象来隔离依赖项带来的影响),也更容易做到编译期检查

自动依赖注入框架

在上面的代码中,由我们编写代码自行创建、提供并管理不同类型的依赖项,这种方式称之为手动依赖注入。与之相应的还有一种自动依赖注入,自动依赖注入使用反射或者编译期代码生成等方案自动的帮助我们创建和提供依赖项,以减少使用依赖注入时需要编写的大量的样板代码。

class Car {
    @Inject
    latinit var engine: Engine
    fun start() {
        engine.start()
    }
}

在Android中这方面最具有代表性的库是Dagger及其Kotlin版本Hilt。

参考