背景
最近因为工作需要,研究了vscode的代码,看文档时发现其中提到了Dependency Injection,并且在类的构造函数中看到了这样的写法。
constructor(
id: string,
@IMessageService messageService: IMessageService,
@IStorageService storageService: IStorageService,
@ITelemetryService telemetryService: ITelemetryService,
@IContextMenuService contextMenuService: IContextMenuService,
@IPartService partService: IPartService,
@IKeybindingService keybindingService: IKeybindingService,
@IInstantiationService instantiationService: IInstantiationService,
@IThemeService themeService: IThemeService,
) {
...
}
因为对此依赖注入比较陌生,特地去了解了一番,故有此文章。
注意:此文章从前端角度说明依赖注入,非前端同学可以绕道
什么是依赖注入?
总结的来说,依赖注入是一种设计模式,因为它解决的是一类问题,这类问题是与依赖相关的。
依赖倒转原则
要知道依赖注入是解决什么问题,我们需要先了解一个原则:依赖倒转原则。
这是设计模式的六大原则之一,其核心是面向接口编程。
它要求我们组织代码时:
- 高层模块不应该依赖低层模块。两个都应该依赖抽象
- 抽象不应该依赖细节
- 细节应该依赖抽象
意思是我们编程时,对系统进行模块化,两个模块之间有依赖,例如模块A依赖模块B,那么根据依赖倒转原则,我们开发时,模块A应该依赖模块B的接口,而不是依赖模块B的实现。
下图描述了此原则提倡的关系:
注意:虽然模块A只依赖接口编程,但在运行的时候,它还是需要有一个具体的模块来负责模块A需要的功能的,所以模块A在【运行时】是需要一个【真的】模块B,而不是它的接口。
所以上图中,Module和Interface之间的线是包含,而不是关联。
前端中的依赖注入
对前端来说,一般较少有抽象,如果不是使用Typescript的话,就更少接触接口了。
但是,依赖注入却是一直都存在,只是许多同学没有认出来而已。
看下面这个栗子,是前端普遍存在的依赖注入
// moduleA.js
define('moduleA', ['moduleB'], function(moduleB) {
return {
init: function() {
this.I_need = ModuleB.someFun();
}
};
});
简单来说,依赖注入就做两件事情
- 初始化被依赖的模块
- 注入到依赖模块中
再看一次栗子:
一般来说,在开发时,我们需要一个对象的能力,一般是自己去创建一个。
那如果,我需要很多个对象,而我需要的对象又依赖了其他对象,这时我是不是得对每个对象都创建一遍。
假设对象 A 依赖 B , C , D, 同时 B 又依赖 E。
如图:
// B 依赖 E
class B {
public constructor(public e: E) {
}
}
class C {
}
class D {
}
class E {
}
// A 依赖 B,C,D
class A {
constructor(public b: B, public c: C, public d: D) {
}
}
创建一个a的实例:
var e = new E();
var b = new B(e);
var c = new C();
var d = new D();
var a = new A(b,c,d);
可以看到,为了创建类A的一个实例,我们需要先创建 B,C,D 的实例,而为了创建一个B的实例,我们又需要创建一个E的实例。如果依赖的e模块做了改变,构造时需要多传入一个参数,那么我们的代码也得跟着改变,随着项目发展,工程越来越大,代码将会变得非常不好维护。
这是我们很会自然想到,有没方法使依赖模块与被依赖模块的初始化信息解耦。
实现依赖注入
其实,js实现依赖注入的方式有多种,简单列举一些:
- 基于Injector、Cache和函数参数名的依赖注入
- AngularJS中基于双Injector的依赖注入
- inversify.js——Javascript技术栈中的IoC容器
- TypeScript中基于装饰器和反射的依赖注入
- ......
那现在我用在工作中使用的模式来讲,也是基于Typescript的装饰器,但不需要用到反射。
先看下如果使用了依赖注入后,模块A是如何使用其他模块的。
import { instantiationService } from 'instantiationService'
class A {
constructor(
@IB b, //模块B
@IC c, //模块C
@ID d, //模块D
) {
...
}
public method1(){
b.xxx() //调用b模块的某个方法
}
}
let a = instantiationService.createInstance(A)
是不是很简单明了,以后使用A模块的能力时,只需要实例化A(实例化时需要使用封装的方法),就不需要管他依赖的参数了。
那么这是如何实现的呢,下面来看下具体方式。
首先,每个类都需要依照自己的接口来实现,接口需要传入createDecorator,此方法会返回一个装饰器,以B模块为例:
// 模块B
import { createDecorator } from 'instantiation';
const IB = createDecorator<IB>('bService');
// 接口在Typescript中只在编译检查中使用,编译后的代码中不会存在
interface IB {
_variable:any;
method1(title: string): void;
method2(title: string): void;
}
class B implements IB{
public _variable:any;
method1(title: string): void{
...
}
method2(title: string): void{
...
}
}
// instantiation.js
function storeServiceDependency(id: Function, target: Function, index: number, optional: boolean): void {
if (target[_util.DI_TARGET] === target) {
target[_util.DI_DEPENDENCIES].push({ id, index, optional });
} else {
target[_util.DI_DEPENDENCIES] = [{ id, index, optional }];
target[_util.DI_TARGET] = target;
}
}
/**
* 此方法会返回一个装饰器
*/
export function createDecorator<T>(serviceId: string): { (...args: any[]): void; type: T; } {
if (_util.serviceIds.has(serviceId)) {
return _util.serviceIds.get(serviceId);
}
const id = <any>function (target: Function, key: string, index: number): any {
if (arguments.length !== 3) {
throw new Error('@IServiceName-decorator can only be used to decorate a parameter');
}
storeServiceDependency(id, target, index, false);
};
id.toString = () => serviceId;
_util.serviceIds.set(serviceId, id);
return id;
}
// instantiationService.js
class InstantiationService implements IInstantiationService {
_serviceBrand: any;
private _services: ServiceCollection;
private _strict: boolean;
constructor(services: ServiceCollection = new ServiceCollection(), strict: boolean = false) {
this._services = services;
this._strict = strict;
this._services.set(IInstantiationService, this);
}
createChild(services: ServiceCollection): IInstantiationService {
this._services.forEach((id, thing) => {
if (services.has(id)) {
return;
}
// If we copy descriptors we might end up with
// multiple instances of the same service
if (thing instanceof SyncDescriptor) {
thing = this._createAndCacheServiceInstance(id, thing);
}
services.set(id, thing);
});
return new InstantiationService(services, this._strict);
}
invokeFunction<R>(signature: (accessor: ServicesAccessor, ...more: any[]) => R, ...args: any[]): R {
let accessor: ServicesAccessor;
try {
accessor = {
get: <T>(id: ServiceIdentifier<T>, isOptional?: typeof optional) => {
const result = this._getOrCreateServiceInstance(id);
if (!result && isOptional !== optional) {
throw new Error(`[invokeFunction] unkown service '${id}'`);
}
return result;
}
};
return signature.apply(undefined, [accessor].concat(args));
} finally {
accessor.get = function () {
throw illegalState('service accessor is only valid during the invocation of its target method');
};
}
}
createInstance<T>(param: any, ...rest: any[]): any {
if (param instanceof AsyncDescriptor) {
// async
return this._createInstanceAsync(param, rest);
} else if (param instanceof SyncDescriptor) {
// sync
return this._createInstance(param, rest);
} else {
// sync, just ctor
return this._createInstance(new SyncDescriptor(param), rest);
}
}
private _createInstanceAsync<T>(descriptor: AsyncDescriptor<T>, args: any[]): TPromise<T> {
let canceledError: Error;
return new TPromise((c, e, p) => {
require([descriptor.moduleName], (_module?: any) => {
if (canceledError) {
e(canceledError);
}
if (!_module) {
return e(illegalArgument('module not found: ' + descriptor.moduleName));
}
let ctor: Function;
if (!descriptor.ctorName) {
ctor = _module;
} else {
ctor = _module[descriptor.ctorName];
}
if (typeof ctor !== 'function') {
return e(illegalArgument('not a function: ' + descriptor.ctorName || descriptor.moduleName));
}
try {
args.unshift.apply(args, descriptor.staticArguments()); // instead of spread in ctor call
c(this._createInstance(new SyncDescriptor<T>(ctor), args));
} catch (error) {
return e(error);
}
}, e);
}, () => {
canceledError = canceled();
});
}
private _createInstance<T>(desc: SyncDescriptor<T>, args: any[]): T {
// arguments given by createInstance-call and/or the descriptor
let staticArgs = desc.staticArguments().concat(args);
// arguments defined by service decorators
let serviceDependencies = _util.getServiceDependencies(desc.ctor).sort((a, b) => a.index - b.index);
let serviceArgs: any[] = [];
for (const dependency of serviceDependencies) {
let service = this._getOrCreateServiceInstance(dependency.id);
if (!service && this._strict && !dependency.optional) {
throw new Error(`[createInstance] ${desc.ctor.name} depends on UNKNOWN service ${dependency.id}.`);
}
serviceArgs.push(service);
}
let firstServiceArgPos = serviceDependencies.length > 0 ? serviceDependencies[0].index : staticArgs.length;
// check for argument mismatches, adjust static args if needed
if (staticArgs.length !== firstServiceArgPos) {
console.warn(`[createInstance] First service dependency of ${desc.ctor.name} at position ${
firstServiceArgPos + 1} conflicts with ${staticArgs.length} static arguments`);
let delta = firstServiceArgPos - staticArgs.length;
if (delta > 0) {
staticArgs = staticArgs.concat(new Array(delta));
} else {
staticArgs = staticArgs.slice(0, firstServiceArgPos);
}
}
// now create the instance
const argArray = [desc.ctor];
argArray.push(...staticArgs);
argArray.push(...serviceArgs);
const instance = create.apply(null, argArray);
desc._validate(instance);
return <T>instance;
}
private _getOrCreateServiceInstance<T>(id: ServiceIdentifier<T>): T {
let thing = this._services.get(id);
if (thing instanceof SyncDescriptor) {
return this._createAndCacheServiceInstance(id, thing);
} else {
return thing;
}
}
private _createAndCacheServiceInstance<T>(id: ServiceIdentifier<T>, desc: SyncDescriptor<T>): T {
assert.ok(this._services.get(id) instanceof SyncDescriptor);
const graph = new Graph<{ id: ServiceIdentifier<any>, desc: SyncDescriptor<any> }>(data => data.id.toString());
function throwCycleError() {
const err = new Error('[createInstance] cyclic dependency between services');
err.message = graph.toString();
throw err;
}
let count = 0;
const stack = [{ id, desc }];
while (stack.length) {
const item = stack.pop();
graph.lookupOrInsertNode(item);
// TODO@joh use the graph to find a cycle
// a weak heuristic for cycle checks
if (count++ > 100) {
throwCycleError();
}
// check all dependencies for existence and if the need to be created first
let dependencies = _util.getServiceDependencies(item.desc.ctor);
for (let dependency of dependencies) {
let instanceOrDesc = this._services.get(dependency.id);
if (!instanceOrDesc) {
console.warn(`[createInstance] ${id} depends on ${dependency.id} which is NOT registered.`);
}
if (instanceOrDesc instanceof SyncDescriptor) {
const d = { id: dependency.id, desc: instanceOrDesc };
graph.insertEdge(item, d);
stack.push(d);
}
}
}
while (true) {
let roots = graph.roots();
// if there is no more roots but still
// nodes in the graph we have a cycle
if (roots.length === 0) {
if (graph.length !== 0) {
throwCycleError();
}
break;
}
for (let root of roots) {
// create instance and overwrite the service collections
const instance = this._createInstance(root.data.desc, []);
this._services.set(root.data.id, instance);
graph.removeNode(root.data);
}
}
return <T>this._services.get(id);
}
}
总结
依赖注入(Dependency Injection),是这样一个过程:由于某客户类只依赖于服务类的一个接口,而不依赖于具体服务类,所以客户类只定义一个注入点。在程序运行过程中,客户类不直接实例化具体服务类实例,而是客户类的运行上下文环境或专门组件负责实例化服务类,然后将其注入到客户类中,保证客户类的正常运行。
参考文章: