【译】Android中的依赖注入

1,423 阅读8分钟

依赖注入(DI)是一种广泛用于编程的技术,非常适合Android开发。通过遵循DI的原则,您可以为良好的应用程序架构奠定基础。

使用依赖注入有以下优势:

  • 复用代码
  • 方便重构
  • 方便测试

依赖注入基础

在专门介绍Android中的依赖项注入之前,此页面将更全面地概述依赖项注入的工作原理。

什么是依赖注入

类经常需要引用别的类。例如,一个Car类可能需要一个Engine类的引用。这些被需要的类被称为依赖,在这个例子中,Car类依赖于Engine类的实例。

有三种方式为类获取它需要的对象:

  1. 类构造它需要的依赖。在上面的例子中,Car将创建和初始化它自己的Engine实例。
  2. 在某个地方获取。一些Android API,例如Contextget方法和getSystemService(),都使用这种方式。
  3. 将其作为参数提供。应用程序可以在构造类时提供这些依赖关系,或将它们传递给需要每个依赖关系的函数。在上面的示例中,Car构造函数将接收Engine作为参数。

第三种方式就是依赖注入!使用这种方法你获取类的依赖并且提供给它们,而不是类实例自己去获取他们。

这里有一个例子,没有依赖注入,Car创建自己的Engine依赖。

class Car {

    private Engine engine = new Engine();

    public void start() {
        engine.start();
    }
}


class MyApp {
    public static void main(String[] args) {
        Car car = new Car();
        car.start();
    }
}

这不是依赖注入的例子因为Car正在构建自己的Engine。这可能存在问题,因为:

  • CarEngine紧密耦合。Car的实例使用一种类型的Engine,并且不能轻松使用任何子类或替代实现。如果汽车要构造自己的引擎,那么您将不得不创建两种类型的汽车,而不是仅将同一个Car重复用于GasElectric类型的引擎。
  • Engine的硬依赖使测试更加困难。Car使用了具体的Engine实例,从而防止您使用测试替身来针对不同的测试用例修改Engine
class Car {

    private final Engine engine;

    public Car(Engine engine) {
        this.engine = engine;
    }

    public void start() {
        engine.start();
    }
}


class MyApp {
    public static void main(String[] args) {
        Engine engine = new Engine();
        Car car = new Car(engine);
        car.start();
    }
}

依赖注入的代码是什么样的?代替在初始化每个Car实例时构造它自己的Engine对象,它都在其构造函数中接收Engine对象作为参数:

class Car {

    private final Engine engine;

    public Car(Engine engine) {
        this.engine = engine;
    }

    public void start() {
        engine.start();
    }
}


class MyApp {
    public static void main(String[] args) {
        Engine engine = new Engine();
        Car car = new Car(engine);
        car.start();
    }
}

main函数使用Car。因为Car依赖Engine,应用创建一个Engine实例并且使用它去构建一个Car实例。这种基于DI的方法的好处是:

  • Car的可复用性。你可以给Car传递不同的Engine实现。例如,你可以定义一个Engine类的新的子类ElectricEngine。如果你使用DI,那么您所要做的就是传入更新的ElectricEngine子类的实例,而Car仍然可以正常工作,而无需进行任何进一步的更改
  • Car方便测试。你可以传递测试替身来测试你不同的场景。例如,你可以创建一个Engine的测试替身称作FakeEngine并且在不同的测试中配置他。

Android中有两种主要方法可以进行依赖项注入:

  • 构造注入。这是上面描述的方法,你将类的依赖传递给他的构造函数。
  • 字段注入。系统会实例化某些Android框架类(例如ActivityFragment),因此无法进行构造函数注入。使用字段注入,在创建类之后实例化依赖项。代码如下所示:
class Car {

    private Engine engine;

    public void setEngine(Engine engine) {
        this.engine = engine;
    }

    public void start() {
        engine.start();
    }
}

class MyApp {
    public static void main(String[] args) {
        Car car = new Car();
        car.setEngine(new Engine());
        car.start();
    }
}

注意:依赖项注入基于控制反转原则,在该原则中,通用代码控制特定代码的执行。

自动依赖注入

在前面的例子中,你不依赖库,自己创建,提供和管理不同类的依赖。这被称作dependency injection by hand或者manual dependency injection(手动依赖注入)。在Car的例子中,只有一个依赖, 但是更多的依赖项和类会使手动注入依赖项变得更加乏味。手动依赖项注入还存在一些问题:

  • 对于大型应用程序,获取所有依赖项并正确连接它们可能需要大量的样板代码。在多层体系结构中,为了为顶层创建对象,您必须提供其下层的所有依赖关系。举一个具体的例子,要制造一辆真正的汽车,您可能需要引擎,变速箱,底盘和其他零件。发动机则需要气缸和火花塞。
  • 当您无法在传递依赖项之前构造依赖项时(例如,在使用延迟初始化或将对象作用域确定为应用程序流时),您需要编写并维护一个自定义容器(或依赖关系图),以管理您的生命周期内存中的依赖项。

有一些库通过自动化创建和提供依赖项的过程来解决此问题。它们分为两类

  • 基于反射的解决方案,它在运行时连接依赖关系。
  • 静态解决防范,它在编译时生成连接依赖的代码。

Dagger是受欢迎的Java、Kotlin和Android依赖注入框架,由Google维护。Dagger通过为您创建和管理依赖关系图来促进在应用程序中使用DI。解决了基于反射的解决方案(例如Guice)的许多开发和性能问题。

依赖注入的可选方案

一个依赖项注入的可选方案是使用service locatorservice locator设计模式还改善了类与具体依赖关系的解耦。您创建一个称为service locator的类,该类创建并存储依赖项,然后根据需要提供这些依赖项。

class ServiceLocator {

    private static ServiceLocator instance = null;

    private ServiceLocator() {}

    public static ServiceLocator getInstance() {
        if (instance == null) {
            synchronized(ServiceLocator.class) {
                instance = new ServiceLocator();
            }
        }
        return instance;
    }

    public Engine getEngine() {
        return new Engine();
    }
}

class Car {

    private Engine engine = ServiceLocator.getInstance().getEngine();

    public void start() {
        engine.start();
    }
}

class MyApp {
    public static void main(String[] args) {
        Car car = new Car();
        car.start();
    }
}

service locator模式与依赖注入的区别在于元素的使用方式。使用service locator模式,类可以控制并要求注入对象;通过依赖注入,该应用程序可以控制并主动注入所需的对象。

与依赖注入相比:

  • service locator需要的依赖项集合使代码更难测试,因为所有测试都必须与同一全局service locator进行交互。

  • 依赖项在类中编码实现,而不是在API外面。很难从外部知道一个类需要什么。对Car或service locator中可用的依赖项的更改可能会导致引用失败,从而导致运行时或测试失败。

  • 如果您想将范围限制到整个应用程序的生命周期之外,则管理对象的生存期将更加困难。

为你的app选择正确的技术

如上所述,有几种不同的技术可以管理应用程序的依赖项:

手动依赖项注入仅对相对较小的应用程序有意义,因为它的扩展性很差。当项目变大时,传递对象需要大量样板代码。

service locator以相对较少的样板代码开始,但扩展性也很差。此外,由于测试依赖单例对象,因此测试变得更加困难。

Dagger被构建去扩展,非常适合构建复杂的应用程序。

如果您的小型应用程序似乎很可能会增长,那么您应该考虑在没有太多代码可更改的情况下尽早迁移到Dagger。

您的项目规模是多少?为了决定使用哪种技术,您可以使用屏幕数来判定应用大小。但是,请注意,屏幕数量只是可能影响您的应用大小的众多因素之一。

为你的library选择正确的技术

如果要开发外部SDK或库,则应根据SDK或库的大小在手动DI或Dagger之间进行选择。请注意,如果您使用第三方库进行依赖项注入,则库的大小可能会增加。

总结

依赖注入为您的应用程序提供以下优势:

  • 类的可重用性和依赖关系的解耦:交换依赖关系的实现会更容易。由于控制反转,因此代码重用得到了改善,并且类不再控制如何创建其依赖项,而是可以与任何配置一起使用。
  • 易于重构:依赖关系成为API外面的可验证部分,因此可以在对象创建时或编译时对其进行检查,而不必将其隐藏为实现细节。
  • 易于测试:类不管理其依赖关系,因此在测试它时,您可以传入不同的实现来测试所有不同的情况。

为了完全理解依赖注入的好处,您应该在应用程序中手动尝试它,如“手动依赖注入”所示。

其他资料