依赖注入(DI)是一种广泛用于编程的技术,非常适合Android开发。通过遵循DI的原则,您可以为良好的应用程序架构奠定基础。
使用依赖注入有以下优势:
- 复用代码
- 方便重构
- 方便测试
依赖注入基础
在专门介绍Android
中的依赖项注入之前,此页面将更全面地概述依赖项注入的工作原理。
什么是依赖注入
类经常需要引用别的类。例如,一个Car
类可能需要一个Engine
类的引用。这些被需要的类被称为依赖,在这个例子中,Car
类依赖于Engine
类的实例。
有三种方式为类获取它需要的对象:
- 类构造它需要的依赖。在上面的例子中,
Car
将创建和初始化它自己的Engine
实例。 - 在某个地方获取。一些Android API,例如
Context
的get
方法和getSystemService()
,都使用这种方式。 - 将其作为参数提供。应用程序可以在构造类时提供这些依赖关系,或将它们传递给需要每个依赖关系的函数。在上面的示例中,
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
。这可能存在问题,因为:
Car
和Engine
紧密耦合。Car
的实例使用一种类型的Engine
,并且不能轻松使用任何子类或替代实现。如果汽车要构造自己的引擎,那么您将不得不创建两种类型的汽车,而不是仅将同一个Car
重复用于Gas
和Electric
类型的引擎。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框架类(例如
Activity
和Fragment
),因此无法进行构造函数注入。使用字段注入,在创建类之后实例化依赖项。代码如下所示:
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 locator。service 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外面的可验证部分,因此可以在对象创建时或编译时对其进行检查,而不必将其隐藏为实现细节。
- 易于测试:类不管理其依赖关系,因此在测试它时,您可以传入不同的实现来测试所有不同的情况。
为了完全理解依赖注入的好处,您应该在应用程序中手动尝试它,如“手动依赖注入”所示。