在线文档教程

依赖注入模式

依赖注入(Dependency injection)模式

依赖注入是一个很重要的设计模式。 它使用得非常广泛,以至于几乎每个人都把它简称为 DI

Angular 有自己的依赖注入框架,离开它,你几乎没办法构建出 Angular 应用。

本页会告诉你 DI 是什么,以及为什么它很有用。

当你学会了这种通用的模式之后,就可以转到 Angular 依赖注入 中去看看它在 Angular 应用中的工作原理了。

为什么需要依赖注入?

要理解为什么依赖注入这么重要,不妨先考虑不使用它的一个例子。想象下列代码:

src/app/car/car.ts (without DI)

content_copyexport class Car {  public engine: Engine; public tires: Tires; public description = 'No DI';  constructor() { this.engine = new Engine( this.tires = new Tires( }  // Method using the engine and tires drive() { return `${this.description} car with ` + `${this.engine.cylinders} cylinders and ${this.tires.make} tires.`; }}

Car 类在自己的构造函数中创建了它所需的一切。 这样做有什么问题? 问题在于 Car 类是脆弱、不灵活以及难于测试的。

Car 类需要一个引擎 (engine) 和一些轮胎 (tire),它没有去请求现成的实例, 而是在构造函数中用具体的 EngineTires 类实例化出自己的副本。

如果 Engine 类升级了,它的构造函数要求传入一个参数,这该怎么办? 这个 Car 类就被破坏了,在把创建引擎的代码重写为 this.engine = new Engine(theNewParameter) 之前,它都是坏的。 当第一次写 Car 类时,你不关心 Engine 构造函数的参数,现在也不想关心。 但是,当 Engine 类的定义发生变化时,就不得不在乎了,Car 类也不得不跟着改变。 这就会让 Car 类过于脆弱。

如果想在 Car 上使用不同品牌的轮胎会怎样?太糟了。 你被锁定在 Tires类创建时使用的那个品牌上。这让 Car 类缺乏弹性。

现在,每辆车都有它自己的引擎。它不能和其它车辆共享引擎。 虽然这对于汽车来说还算可以理解,但是设想一下那些应该被共享的依赖,比如用来联系厂家服务中心的车载无线电。 这种车缺乏必要的弹性,无法共享当初给其它消费者创建的车载无线电。

当给 Car 类写测试的时候,你就会受制于它背后的那些依赖。 能在测试环境中成功创建新的 Engine 吗? Engine 自己又依赖什么?那些依赖本身又依赖什么? Engine 的新实例会发起到服务器的异步调用吗? 你当然不想在测试期间这么一层层追下去。

如果 Car 应该在轮胎气压低的时候闪动警示灯该怎么办? 如果没法在测试期间换上一个低气压的轮胎,那该如何确认它能正确的闪警示灯?

你没法控制这辆车背后隐藏的依赖。 当不能控制依赖时,类就会变得难以测试。

该如何让 Car 更强壮、有弹性以及可测试?

答案非常简单。把 Car 的构造函数改造成使用 DI 的版本:

src/app/car/car.ts (excerpt with DI)

src/app/car/car.ts (excerpt without DI)

content_copypublic description = 'DI'; constructor(public engine: Engine, public tires: Tires) { }

发生了什么?现在依赖的定义移到了构造函数中。 Car 类不再创建引擎 engine 或者轮胎 tires。 它仅仅“消费”它们。

这个例子又一次借助 TypeScript 的构造器语法来同时定义参数和属性。

现在,通过往构造函数中传入引擎和轮胎来创建一辆车。

content_copy// Simple car with 4 cylinders and Flintstone tires. let car = new Car(new Engine(), new Tires()

酷!引擎和轮胎这两个依赖的定义与 Car 类本身解耦了。 只要喜欢,可以传入任何类型的引擎或轮胎,只要它们能满足引擎或轮胎的通用 API 需求。

这样一来,如果有人扩展了 Engine 类,那就不再是 Car 类的烦恼了。

Car消费者也有这个问题。消费者必须修改创建这辆车的代码,就像这样:

content_copyclass Engine2 { constructor(public cylinders: number) { } } // Super car with 12 cylinders and Flintstone tires. let bigCylinders = 12; let car = new Car(new Engine2(bigCylinders), new Tires()

这里的要点是:Car 本身不必变化。下面就来解决消费者的问题。

Car 类非常容易测试,因为现在你对它的依赖有了完全的控制权。 在每个测试期间,你可以往构造函数中传入 mock 对象,做想让它们做的事:

content_copyclass MockEngine extends Engine { cylinders = 8; } class MockTires extends Tires { make = 'YokoGoodStone'; } // Test car with 8 cylinders and YokoGoodStone tires. let car = new Car(new MockEngine(), new MockTires()

刚刚学习了什么是依赖注入

它是一种编程模式,可以让类从外部源中获得它的依赖,而不必亲自创建它们。

酷!但是,可怜的消费者怎么办? 那些希望得到一个 Car 的人们现在必须创建所有这三部分了:CarEngineTiresCar 类把它的快乐建立在了消费者的痛苦之上。 需要某种机制为你把这三个部分装配好。

可以写一个巨型类来做这件事:

src/app/car/car-factory.ts

content_copyimport { Engine, Tires, Car } from './car'; // BAD pattern!export class CarFactory { createCar() { let car = new Car(this.createEngine(), this.createTires() car.description = 'Factory'; return car; }  createEngine() { return new Engine( }  createTires() { return new Tires( }}

现在只需要三个创建方法,这还不算太坏。 但是当应用规模变大之后,维护它将变得惊险重重。 这个工厂类将变成由相互依赖的工厂方法构成的巨型蜘蛛网。

如果能简单的列出想建造的东西,而不用定义该把哪些依赖注入到哪些对象中,那该多好!

到了依赖注入框架一展身手的时候了! 想象框架中有一个叫做注入器 (injector) 的东西。 用这个注入器注册一些类,它会弄明白如何创建它们。

当需要一个 Car 时,就简单的找注入器取车就可以了。

src/app/car/car-injector.ts

content_copylet car = injector.get(Car

皆大欢喜。Car 不需要知道如何创建 EngineTires。 消费者不需要知道如何创建 Car。 开发人员不需要维护巨大的工厂类。 Car 和消费者只要简单地请求想要什么,注入器就会交付它们。

这就是“依赖注入框架”存在的原因。

现在,你知道什么是依赖注入以及它有什么优点了吧?那就请到 Angular 依赖注入 中去看看它在 Angular 中是如何实现的。