Kotlin 38. Dependency Injection依赖注入以及Hilt在Kotlin中的使用,系列1:依赖注入介绍

一起来学Kotlin:概念:25. Dependency Injection依赖注入以及Hilt在Kotlin中的使用,系列1:依赖注入介绍

此系列博客中,我们将主要介绍:

  • Dependency Injection(依赖注入) 概念介绍。网上看了许多关于 DI 的介绍,云里雾里。这里,我们通过通俗易懂地方式对其进行介绍。
  • 手动依赖注入介绍。为了让大家更容易理解 Hilt,我们先介绍如何通过手动的方式实现依赖注入效果。
  • Hilt 注释(annotations)介绍及使用案例
  • MVVM 案例中如何使用 Hilt

此博客主要介绍Dependency Injection(依赖注入)概念。



1 什么是 DI

所以在开始理解 DI 之前,首先让我们了解编程中的依赖(Dependency)是什么意思。

当 A 类使用 B 类的某些功能时,则表示 A 类具有 B 类的依赖项。在 Java 中,在我们使用其他类的方法之前,我们首先需要创建该类的对象(即类 A 需要创建类 B 的实例)。

简单来说,依赖注入就是一个对象A需要(依赖于)另外一个对象B的实例。 需要把对象B的实例注入到对象A中,而这个“注入”的动作,要么由程序员手动执行,要么交给软件框架执行。

因此,将创建对象的任务转移给其他人并直接使用依赖称为依赖注入(DI)。

这里再举个例子。

假设我们有一个汽车类,其中包含各种对象,例如车轮、引擎等。在不用依赖注入时,我们只能在汽车(类)里面 new 一个引擎(实例)出来:private val engine = Engine():

class Car {
    
    <!-- -->
    private val engine = Engine()

    fun start() {
    
    <!-- -->
        engine.start()
    }
}

fun main(args: Array) {
    
    <!-- -->
    val car = Car()
    car.start()
}

这样写有一个问题:汽车 Car 类和引擎 Engine 严重耦合。每个 Car 类的实例都使用不同的 Engine 实例。这种汽车 Car 类内部 “建造引擎” 的方式,使得该汽车 Car 类难以测试及维护。如果我们需要把汽车区分为内燃机汽车和电动汽车时,只能去 Car 类的内部修改。这就违反了面向对象软件开发的“开放封闭原则”。

那么 DI风格的代码应该是什么样的呢? Car类在它的构造函数里接收一个 Engine 类型的参数:class Car(private val engine: Engine){ ... }

class Car(private val engine: Engine) {
    
    <!-- -->
    fun start() {
    
    <!-- -->
        engine.start()
    }
}

fun main(args: Array) {
    
    <!-- -->
    val engine = Engine()
    val car = Car(engine)
    car.start()
}

这样汽车 Car 类就不需要自己内部“建造引擎”,它需要什么样的引擎,直接在实例化的时候从外部传(注入)进来就可以了。

在这里,我们就可以说 Car 类的实例化依赖于Engine类的实例,我们在构造 Car 类实例 car(实例对象变量一般首字母小写)的时候传入(注入)了 Engine 类的实例。

这么做的好处也显而易见:

  • Car类可重复使用,你需要一辆燃油车,在实例化的时候传入内燃机引擎;而当需要一辆电动车的时候,传入电动机引擎;
  • Car类的可测试性变成了现实(你可以传入不同类型的FakeEngine做测试 Test Double)。

2 DI 的类型

基本上有三种 DI 类型:

  • constructor injection(类的构造函数注入):依赖项是通过类构造函数提供的。
  • setter injection(类字段注入):客户端公开一个 setter 方法,注入器使用它来注入依赖项。
  • interface injection:依赖项提供了一个注入器方法,可以将依赖项注入传递给它的任何客户端。 客户端必须实现一个接口,该接口公开一个接受依赖项的 setter 方法。

上面的例子就是 constructor injection(类的构造函数注入)。第二种:setter injection(类字段注入),比如下代码片段所示中的 lateinit var engine: Engine

class Car {
    
    <!-- -->
    lateinit var engine: Engine

    fun start() {
    
    <!-- -->
        engine.start()
    }
}

fun main(args: Array) {
    
    <!-- -->
    val car = Car()
    car.engine = Engine()
    car.start()
}

所以现在 DI 的责任是:

  • 创建对象
  • 知道哪些类需要那些对象
  • 并为他们提供所有这些对象

如果对象有任何变化,那么 DI 会调查它,它不应该关注使用这些对象的类。 这样,如果将来对象发生变化,则其 DI 有责任向类提供适当的对象。

顺便说一句,我们之前有一个博客,介绍的是 SOLID 原则(面向对象编程和设计的五个基本原则)。它里面的第五条,类应该依赖于抽象而不是具体化(简单来说,硬编码)。根据这些原则,一个类应该专注于履行其职责,而不是创建履行这些职责所需的对象。 这就是 DI 发挥作用的地方:它为类提供所需的对象。

3 DI 的优劣

DI 的优点:

  • 有助于单元测试。
  • 样板代码减少了,因为依赖项的初始化由注入器组件完成。
  • 扩展应用程序变得更加容易。
  • 有助于实现松散耦合,这在应用程序编程中很重要。

DI 的缺点:

  • 学习起来有点复杂,如果过度使用会导致管理问题和其他问题。
  • 许多编译时(compile time)错误被推送到运行时(run-time)。
  • DI 框架是通过反射或动态编程实现的。 这可能会阻碍 IDE 自动化的使用,例如“查找引用”、“显示调用层次结构”和安全重构。

实现 DI 的库和框架:

  • Spring (Java)
  • Google Guice (Java)
  • Dagger (Java and Android)
  • Castle Windsor (.NET)
  • Unity(.NET)

通过以上的分析,我们对依赖注入(DI)有了一个相对直观的印象。依赖注入(DI)的目的就是为了代码间的解耦,解耦的目的是为了提高代码的可重用性和可测试性,而提高了代码的可重用性和可测试性是为了增强代码的鲁棒性(Robust)。

猜你喜欢

转载自blog.csdn.net/zyctimes/article/details/129218437