依赖注入 in 前端 && Typescript 实现依赖注入

背景

最近因为工作需要,研究了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的实现。

下图描述了此原则提倡的关系:
Fhe152elkbDvQo8SPbpByySnszgN.jpg

注意:虽然模块A只依赖接口编程,但在运行的时候,它还是需要有一个具体的模块来负责模块A需要的功能的,所以模块A在【运行时】是需要一个【真的】模块B,而不是它的接口。

所以上图中,Module和Interface之间的线是包含,而不是关联。

前端中的依赖注入

对前端来说,一般较少有抽象,如果不是使用Typescript的话,就更少接触接口了。

但是,依赖注入却是一直都存在,只是许多同学没有认出来而已。
看下面这个栗子,是前端普遍存在的依赖注入

// moduleA.js
define('moduleA', ['moduleB'], function(moduleB) {
    return {
        init: function() {
            this.I_need = ModuleB.someFun();
        }
    };
});

简单来说,依赖注入就做两件事情

  1. 初始化被依赖的模块
  2. 注入到依赖模块中

再看一次栗子:
一般来说,在开发时,我们需要一个对象的能力,一般是自己去创建一个。
那如果,我需要很多个对象,而我需要的对象又依赖了其他对象,这时我是不是得对每个对象都创建一遍。

假设对象 A 依赖 B , C , D, 同时 B 又依赖 E。
如图:
v2-2830636e440e6aa862dbd31f2bf33f45_hd.jpg

// 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),是这样一个过程:由于某客户类只依赖于服务类的一个接口,而不依赖于具体服务类,所以客户类只定义一个注入点。在程序运行过程中,客户类不直接实例化具体服务类实例,而是客户类的运行上下文环境或专门组件负责实例化服务类,然后将其注入到客户类中,保证客户类的正常运行。

参考文章:

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值