依赖注入模式
依赖注入(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),它没有去请求现成的实例, 而是在构造函数中用具体的 Engine
和 Tires
类实例化出自己的副本。
如果 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
的人们现在必须创建所有这三部分了:Car
、Engine
和 Tires
。 Car
类把它的快乐建立在了消费者的痛苦之上。 需要某种机制为你把这三个部分装配好。
可以写一个巨型类来做这件事:
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
不需要知道如何创建 Engine
和 Tires
。 消费者不需要知道如何创建 Car
。 开发人员不需要维护巨大的工厂类。 Car
和消费者只要简单地请求想要什么,注入器就会交付它们。
这就是“依赖注入框架
”存在的原因。
现在,你知道什么是依赖注入以及它有什么优点了吧?那就请到 Angular 依赖注入 中去看看它在 Angular 中是如何实现的。