依赖注入(DI)
Posted wpengch1
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了依赖注入(DI)相关的知识,希望对你有一定的参考价值。
注:学习使用,禁止转载
通常,随着应用程序的变大,应用程序的不同部分需要交流,当模块a需要模块b才能运行时,我们说b依赖于a。
获得依赖的最常见的一种方式就是导入一个文件,比如,在我们假设的模块中,我们可能向下面这样做:
// in A.ts
import B from 'B'; // a dependency!
B.foo(); // using B
通常情况下,简单导入其他代码就够了,然而有时候,我们所需要的是一种更复杂的方式提供依赖项。比如:
- 在测试期间,我们需要实现B代替MockB。
- 我们想要分享我们B类的单例在我们的app中(比如单例模式)
- 当每次b被使用时,需要创建一个新的实例
DI可以解决这些问题
DI是这样一个系统,它使我们程序的一部分可以访问其他部分,而且,我们可以配置这些事怎么发生的。
DI这个词是用来描述设计模式的(它使用在很多框架中),angular内建就实现了DI。
使用DI的主要好处是,客户组件不需要了解如何创建依赖,只需要知道怎么交互就可以了。4
DI的例子:PriceService
想象一下,我们有一个Product类,这个Product有一个价格,为了计算这个产品的最终价格,我们依赖于一个服务,它使用输入传递进来。
- 产品的基本价格
- 我们销售的状态
下面是不使用DI的样式:
class Product
constructor(basePrice: number)
this.service = new PriceService();
this.basePrice = basePrice;
price(state: string)
return this.service.calculate(this.basePrice, state);
现在,让我们想象一下,我们需要为这个Product编写一个测试,假设PriceService通过查询数据库返回一个折扣,我们写的测试像下面这样:
let product;
beforeEach(() =>
product = new Product(11);
);
describe('price', () =>
it('is calculated based on the basePrice and the state', () =>
expect(product.price('FL')).toBe(11.66);
);
)
即使这个测试可以工作,这种方法有一些缺点,为了测试成功,必须有几个条件得到满足:
- 数据库必须运行
- Florida的税收收入必须是我们期望的。
基本上,如果我们增加一个意想不到的依赖,我们的测试会更加脆弱。
如果我们做一点改变,像下面这样:
class Product
constructor(service: PriceService, basePrice: number)
this.service = service;
this.basePrice = basePrice;
price(state: string)
return this.service.calculate(this.basePrice, state);
现在,创建一个Product,客户需要决定使用哪一种价格服务。
我们可以编写一个简单的测试,如下:
class MockPriceService
calculate(basePrice: number, state: string)
if (state === 'FL')
return basePrice * 1.06;
return basePrice;
通过这个小小的改变,我们可以调整我们的测试使其更清楚,而且摆脱了数据的依赖。
let product;
beforeEach(() =>
const service = new MockPriceService();
product = new Product(service, 11);
);
describe('price', () =>
it('is calculated based on the basePrice and the state', () =>
expect(product.price('FL')).toBe(11.66);
);
)
我们还会得到好处,我们的 Product类是隔离的,也就是说,我们的Product有一个确切的依赖。
Don’t Call Us…”别叫我
DI这个技术依赖于控制反转这种原则。
多年来,这是很常见的的模式,我们为每一个组件注明完整的应用程序上下文,并且设置它的依赖,这看起来是很清晰的,Product类必须知道PriceService类。
这样做的一个缺点是,一旦一个组件需要一个依赖,组件本身变得脆弱而且难以改变。如果我们做出变化,我们的组件依赖于很多其他组件,我们不得不传播这些依赖。换句话说,它使得我们的组件紧耦合。
当我们使用DI的时候,我们朝着松耦合的方向前进,单个应用程序尽量少影响其他的组件。并且,只要这些组件的接口没有改变,我们甚至可以直接改变它们,不需要组件做任何其他的改变。
ng2继承了ng1的一大特点就是控制反转。angular使用DI去解决来自于外部的依赖。
传统上,如果组件a依赖组件b,那么在组件a的内部会创建一个b的对象,这意味着a依赖于b,
angular使用DI改变这个事情,A像依赖注入系统请求B,依赖注入系统会像我们期望一样传递一个B给A。
相对传统方式来说,这个有很多好处。一个好处就是当我们测试A的时候,我们可以mock一个B出来注入到A。
在这本书中,我们使用了很多次DI。比如我们在路由那张创建music应用的时候,为了去和Spotify API进行交互,我们创建了SpotifyService,它被注入到了几个组件中:
code/routes/music/app/ts/components/AlbumComponent.ts
constructor(public routeParams: RouteParams, public spotify: SpotifyService,
public locationStrategy: LocationStrategy)
this.id = routeParams.get('id');
现在,让我们学习怎么创建自己的service和我们注入它们的不同形式。
依赖注入部分
注册一个依赖,我们必须将其绑定到能标识依赖的地方,这个标识被称为依赖令牌(token),比如,我们想要注册一个API的URL,我们可以使用API_URL作为令牌,同理,如果我们注册一个类,我们可以使用类本身作为它的令牌。
在angular中的依赖注入,分为三步部分:
- 提供者(Provider-通常也称为绑定)映射一个令牌(通常是一个string或者一个类)到一个列表中,它告诉angular,怎么去创建一个对象,并且给定一个令牌。
- 注入器(Injector),它存储绑定的集合,并在创建对象的时候去解析依赖并且注入它们
- 依赖(Dependency),将被注入的东西
我们可以通过下面这张图描述它们的职责:
当处理DI的时候,有很多不同的选择,让我们逐一学习它们。
使用DI最通用的场景就是在整个应用程序中提供服务或者值,这个场景占了99%。
这个就是我们想要去做的,接下来,我们会讲解怎样编写一个基本的服务,它会是我们在用户程序中花费时间最多的。
注入器
像上面提到的一样,angular在后台为我们安装一个DI,在我们使用注解注入依赖到我们的组件之前,让我们首先了解一下注入器。
我们创建一个仅仅返回一个string的简单服务。
code/dependency_injection/simple/app/ts/app.ts
class MyService
getValue():string
return 'a value';
接下来,我们创建我们的APP组件:
code/dependency_injection/simple/app/ts/app.ts
@Component(
selector: 'di-sample-app',
template: `
<button (click)="invokeApi()">Get a value</button>
`
)
class DiSampleApp
myService: MyService;
constructor()
let injector: any = ReflectiveInjector.resolveAndCreate([MyService]);
this.myService = injector.get(MyService);
console.log('Same instance?', this.myService === injector.get(MyService));
invokeApi():void
console.log('MyService returned', this.myService.getValue());
这个程序很简单,我们定义一个DiSampleApp的组件,它会显示一个button,当button被点击的时候,调用invokeApi。
现在,我们关注一下构造函数,我们使用一个来自于ReflectiveInjector的静态方法resolveAndCreate.这个方法的职责是创建一个新的注入器,参数是一个我们想要这个注入器知道的可注入的(injectable)东西的数组,在这个例子中,我只想要它知道MyService。
ReflectiveInjector是Injector的具体实现,它使用反射去寻找合适的参数类型,相对于其他的注入器,ReflectiveInjector是一个通用的注入器。
一个重要的事情是,它将会注入这个类的单实例(single)
这个可以通过构造函数的最后两行得以确定,
console.log('Same instance?', this.myService === injector.get(MyService));
这句话证明,我们使用injector.get获取的两次实例是相同的。
注意,如果我们使用自己的注入器,我们就不需要告诉应用程序的可注入列表。
既然我们学会了注入器的工作原理,然后我们学习angular的注入框架,接下来学习我们可以注入的其他东西,并且学会怎么注入它们。
Providers(提供者)
接下来需要知道的,在angular的DI系统中,我们可以使用几种注入方式:
- 注入一个单例
- 调用任何函数,然后注入这个函数的返回值
- 注入一个值
- 创建一个别名
让我们分开看看
使用一个类
注入一个类的单实例可能是最通常做的事情。下面是我们配置它的代码:
provide(MyComponent, useClass: MyComponent)
值得注意的是,provide方法接收两个参数,第一个就是令牌,第二个是怎样注入或者注入什么。所以这里,我们将我们的MyComponent类映射到MyComponent令牌,在这种情况下,类的名字与令牌匹配,这是通常的情形,但是要注意,被注入的东西和令牌不一定要有相同的名字。
就像我们在上面看到的一样,这个方法会创建一个单例对象,并且我们每次注入的时候它都返回这个相同的单例,当然,在第一次注入之前,不会创建这个类的实例,所以第一次注入的时候,会调用该类的构造函数。
如果一个服务的构造函数需要参数怎么办呢?
code/dependency_injection/misc/app/ts/app.ts
class ParamService
constructor(private phrase: string)
console.log('ParamService is being created with phrase', phrase);
getValue(): string
return this.phrase;
注意,这个构造器怎么获取参数呢?如果我们使用正常的注入机制:
bootstrap(MyApp, [ParameterService]);
我们会得到一个错误:
这个发生的原因是我们没有给注入器关于构建一个类的足够的信息,为了解决这个问题,我们需要告诉注入器,当创建这个服务的实例时,我们想要它去使用哪些参数。
如果我们希望创建服务的时候传递参数,我们可能需要使用工厂来代替(factory)
工厂(Using a Factory)
当我们使用一个工厂注入器的时候,我们可以编写一个函数,这个函数可以返回任何类型。
provide(MyComponent, useFactory: () =>
if (loggedIn)
return new MyLoggedComponent();
return new MyComponent();
);
在上面的代码中,我们注入MyComponent令牌,但是这个会检测一个外部变量loggedIn,如果这个参数为真,我们会接受一个MyLoggedComponent的实例,否则返回MyComponent。
工厂也可以有依赖,比如:
provide(MyComponent, useFactory: (user) =>
if (user.loggedIn())
return new MyLoggedComponent(user);
return new MyComponent();
, deps: [User])
如果我们希望像上面一样使用我们的ParamService,我们可以将他包装在useFactory中,像下面这样:
bootstrap(DiSampleApp, [
SimpleService,
provide(ParamService,
useFactory: (): ParamService => new ParamService('YOLO')
)
]).catch((err: any) => console.error(err));
对于创建注入来说,工厂是功能最强大的方式,因为我们可以在函数中做任何事情。
使用一个值
当我们想要一个值,在应用程序的另外一个地方会被重定义的时候,注入一个值是非常有用的。比如:
provide('API_URL', useValue: 'http://my.api.com/v1')
用于定义别名(alias)
我们也可以用于定义一个引用的别名,像下面这样:
provide(NewComponent, useClass: MyComponent)
在应用程序中的依赖注入
在编写应用程序时,为了注入服务,需要执行下面三步:
- 创建一个服务类
- 在接收组件里面声明一个依赖
- 配置注入(使用angular注册一个注入)
我们要做的第一件事情就是创建一个服务类,这个类可以暴露一些我们希望的行为,这个被称为注射,它包含一些我们组件类将要从诸如服务那里获取到的行为。
下面是我们创建的一个服务:
code/dependency_injection/simple/app/ts/services/ApiService.ts
export class ApiService
get(): void
console.log('Getting resource...');
现在,我们有了要注入的类,接下来就是在我们的组件中声明一个依赖,前面我们直接使用Injector类,但是angular为我们提供了两种简便的方式,最经典的方式就是在我们的构造器中声明。
为了做这个事情,首先需要引入服务:
code/dependency_injection/simple/app/ts/app.ts
/*
* Services
*/
import ApiService from 'services/ApiService';
然后,我们在构造器中声明它:
code/dependency_injection/simple/app/ts/app.ts
class DiSampleApp
constructor(private apiService:ApiService)
当我们在构造器中声明之后,angular会通过反射查找要注入的服务,并注入进来。也就是说,angular会通过我们在构造器中声明的对象类型寻找ApiService,然后向DI系统接收一个合适的注入。
有时,我们需要给angular更多的提示,这个时候,我们使用@Inject注解。
class DiSampleApp
private apiService: ApiService;
constructor(@Inject(ApiService) apiService)
this.apiService = apiService;
使用依赖注入的最后一件事情就是使用injectable链接我们组件想要的注入的东西。换句话说,我们要告诉angular当一个组件声明它的依赖的时候,哪些东西需要注入。
provide(ApiService, useClass: ApiService)
在上面的代码中,我们使用ApiService这个token去导出ApiService的单实例。
与Injectors工作
我们已经可以使用injectors做一些事情,现在我们讨论,我们什么时候需要显示使用他们
一种情况就是当我们希望控制依赖注入的单实例创建的时候,为了描述这个情况,我们使用ApiService创建另外一个应用程序。
这个服务会使用到其他的两个服务,它是基于浏览器视框。当小于800像素的时候,返回一个叫着SmallService,否则,返回一个LargeService.。
SmallService像下面这样:
code/dependency_injection/complex/app/ts/services/SmallService.ts
export class SmallService
run(): void
console.log('Small service...');
LargeService:像这样:
code/dependency_injection/complex/app/ts/services/LargeService.ts
export class LargeService
run(): void
console.log('Large service...');
然后,我们编写ViewPortService,它选择两者之一:
code/dependency_injection/complex/app/ts/services/ViewPortService.ts
import LargeService from './LargeService';
import SmallService from './SmallService';
export class ViewPortService
determineService(): any
let w: number = Math.max(document.documentElement.clientWidth,
window.innerWidth || 0);
if (w < 800)
return new SmallService();
return new LargeService();
现在,让我们创建一个访问我们服务的APP,
code/dependency_injection/complex/app/ts/app.ts
class DiSampleApp
constructor(private apiService: ApiService,
@Inject('ApiServiceAlias') private aliasService: ApiService,
@Inject('SizeService') private sizeService: any)
这里,我们获取了一个ApiService的实例,但是我们又获取了一个ApiService的服务,并给它取一个别名ApiServiceAlias,然后,我们获取一个SizeService,但是它还没有定义。
为了理解每一个service代表什么,让我们看看bootstrap的代码:
bootstrap(DiSampleApp, [
ApiService,
ViewPortService,
provide('ApiServiceAlias', useExisting: ApiService),
provide('SizeService', useFactory: (viewport: any) =>
return viewport.determineService();
, deps: [ViewPortService])
]).catch((err: any) => console.error(err));
这里,我们希望应用程序注入器知道ApiService和ViewPortService。然后,我们定义了ApiService的另外一个别名ApiServiceAlias。然后,我们定义了另外一个注入叫着SizeService.这个工厂接收一个ViewPortService的实例,将它作为依赖放入列表,然后调用它的determineService,它会根据浏览器的宽度返回SmallService或者LargeService。
当我们在app上点击button的时候,我们希望去做三个调用: 一个是ApiService,一个是ApiServiceAlias,一个是SizeService:。
code/dependency_injection/complex/app/ts/app.ts
invokeApi(): void
this.apiService.get();
this.aliasService.get();
this.sizeService.run();
现在,让我们运行我们的app,如果在小的浏览器上:
如果在大的浏览器上:
可以看到,两次打印的信息不一样,说明获取到了不同的服务。
但是,现在,如果我们缩放浏览器,并点击button,它始终会打印Large。
这是因为,factory只会被调用一次,为了解决这个问题,我们创建我们自己的注入器,它获取适当的服务,像下面这样:
code/dependency_injection/complex/app/ts/app.ts
let injector:any = ReflectiveInjector.resolveAndCreate([
ViewPortService,
provide('OtherSizeService',
useFactory: (viewport:any) =>
return viewport.determineService();
, deps: [ViewPortService]
)
]);
let sizeService:any = injector.get('OtherSizeService');
sizeService.run();
这里,我们创建一个注入器,它知道ViewPortService,和另外的OtherSizeService作为令牌的注入,最后我们使用获取OtherSizeService的实例。现在如果我们缩放窗口,点击button,我们会得到合适的日志,因为每次点击button的时候,都会调用useInjectors一次。
替换值
使用DI的另一个原因就是在运行时替换值。当我们有一个APIService,它执行HTPP请求的时候发生。在我们的单元测试或者继承测试中,我们不希望我们的代码访问数据库,在这种情况下,我们希望编写一个mock service来代替我们的真正实现。
比如,如果我们希望在开发环境和生产环境,我们的APP访问不同的api。
或者是,当我们发布一个开源或者可重用的API服务时,我们希望客户端应用程序定义或者重载我们API URL。
让我们编写一个应用程序,它根据我们是在开发环境还是在生产环境返回不同的值。
code/dependency_injection/value/app/ts/services/ApiService.ts
import Inject from '@angular/core';
export const API_URL: string = 'API_URL';
export class ApiService
constructor(@Inject(API_URL) private apiUrl: string)
get(): void
console.log(`Calling $this.apiUrl/endpoint...`);
我们定义一个常量,它会被作为令牌为API URL的依赖使用。换句话说,angular会使用API URL来存储调用的URL信息,这样,当我们使用@Inject(API_URL)的时候,就会返回合适的值,注意,我们导出了API_URL常量,所以客户端应用程序可以使用。
现在,已经有了service,让我们编写一个使用这个服务的组件,它根据不同的运行环境提供不同的值。
code/dependency_injection/value/app/ts/app.ts
@Component(
selector: 'di-value-app',
template: `
<button (click)="invokeApi()">Invoke API</button>
`
)
class DiValueApp
constructor(private apiService: ApiService)
invokeApi(): void
this.apiService.get();
这是组件的代码,在构造器中,我们使用了注入apiService服务,如果想要显示声明,可以使用下面这种方式:
constructor(@Inject(ApiService) private apiService: ApiService)
在这个组件中有一个button,当我们点击button的时候,调用apiService的get方法,并打印返回值,也就是API_URL的值。
接下来是使用providers配置应用程序。
code/dependency_injection/value/app/ts/app.ts
bootstrap(DiValueApp, [
provide(ApiService, useClass: ApiService ),
provide(API_URL,
useValue: isProduction ?
'https://production-api.sample.com' :
'http://dev-api.sample.com'
)
]).catch((err: any) => console.error(err));
首先,我们声明一个常量isProduction,并且设置它为false,我们可以假装我们做一些事情来决定我们是不是在生产环境。这个设置可以是像我们一样硬编码,也可以使用像webpack一样的设置。
最后,我们启动应用程序,并且按照两个提供程序,一个是ApiService,另外一个是API_URL,如果我们是在生产环境中,我们会使用一个值,否则使用另外一个值。
为了测试,可以将isProduction设置为true,并且点击button,会得到下面两种输出。
总结
就像你所看到的,管理应用程序依赖方面,DI是一个功能强大的方式。
以上是关于依赖注入(DI)的主要内容,如果未能解决你的问题,请参考以下文章