VS Code源码深入浅出--依赖注入设计

在阅读 vs code 代码的过程中,我们会发现每一个模块中都有大量装饰器的使用,用来装饰模块以及其中依赖的模块变量。这样做的目的是什么呢?在这一篇中我们来详细分析一下。
依赖注入介绍
如果有这样一个模块 a,它的实现依赖另一个模块 b 的能力,那么应该如何设计呢?很简单,我们可以在 a 模块的构造函数中实例化模块 b,这样就可以在模块 a 内部使用模块 b 的能力了。
class a {  constructor() {    this.b = new b();  }}class b {}const a = new a();  
但是这样做有两个问题,一是模块 a 的实例化过程中,需要手动实例化模块 b,而且如果模块 b 的依赖关系发生变化,那么也需要修改模块 a 的构造函数,导致代码耦合。
二是在复杂项目中,我们在实例化模块 a 时,难以判断模块 b 是否被其他模块依赖而已经实例化过了,从而可能将模块 b 多次实例化。若模块 b 较重或者需要为单例设计,这将带来性能问题。
因此,更好的方式是,将所有模块的实例化交给外层框架,由框架统一管理模块的实例化过程,这样就可以解决上述两个问题。
class a {  constructor(private b: b) {    this.b = b;  }}class b {}class c {  constructor(private a: a, private b: b) {    this.b = b;  }}const b = new b();const a = new a(b);const c = new c(a, b);  
这种将依赖对象通过外部注入,避免在模块内部实例化依赖的方式,称为依赖注入 (dependencies inject, 简称 di)。这在软件工程中是一种常见的设计模式,我们在 java 的 spring,js 的 angular,node 的 nestjs 等框架中都可以看到这种设计模式的应用。
当然,在实际应用中,由于模块众多,依赖复杂,我们很难像上面的例子一样,规划出来每个模块的实例化时机,从而编写模块实例化顺序。并且,许多模块可能并不需要第一时间被创建,需要按需实例化,因此,粗暴的统一实例化是不可取的。
因此我们需要一个统一的框架来分析并管理所有模块的实例化过程,这就是依赖注入框架的作用。
借助于 typescript 的装饰器能力,vscode 实现了一个极为轻量化的依赖注入框架。我们可以先来简单实现一下,解开这个巧妙设计的神秘面纱。
最简依赖注入框架设计
实现一个依赖注入框架只需要两步,一个是将模块声明并注册到框架中进行管理,另一个是在模块构造函数中,声明所需要依赖的模块有哪些。
我们先来看模块的注册过程,这需要 typescript 的类装饰器能力。我们在注入时,只需要判断模块是否已经注册,如果没有注册,将模块的 id(这里简化为模块 class 名称)与类型传入即可完成单个模块的注册。
export function injectable(): classdecorator {  return (target: class): any => {    if (!collection.providers.has(target.name)) {      collection.providers.set(target.name, target);    }    return target;  };}  
之后我们再来看看模块是如何声明依赖的,这需要 typescript 的属性装饰器能力。我们在注入时,先判断依赖的模块是否已经被实例化,如果没有,则将依赖模块进行实例化,并存入框架中管理。最终返回已经被实例化完成的模块实例。
export function inject(): propertydecorator {  return (target: property, propertykey: string) => {    const instance = collection.dependencies.get(propertykey);    if (!instance) {      const dependencyprovider: class = collection.providers.get(propertykey);      collection.dependencies.set(propertykey, new dependencyprovider());    }    target[propertykey] = collection.dependencies.get(propertykey);  };}  
最后只需要保证框架本身在项目运行前完成实例化即可。(在例子中表示为 injector)
export class servicecollection {  readonly providers = new map();  readonly dependencies = new map();}const collection = new servicecollection();export default collection;  
这样,一个最简化的依赖注入框架就完成了。由于保存了模块的类型与实例,它实现了模块的按需实例化,无需在项目启动时就初始化所有模块。
我们可以尝试调用它,以上面举出的例子为例:
@injectable()class a {  constructor(@inject() private b: b) {    this.b = b;  }}@injectable()class b {}class c {  constructor(@inject() private a: a, @inject() private b: b) {    this.b = b;  }}const c = new c();  
无需知晓模块 a,b 的实例化时机,直接初始化任何一个模块,框架会自动帮你找到并实例化好所有依赖的模块。
vscode 的依赖收集实现
上面介绍了一个依赖注入框架的最简实现。但当我们真正阅读 vscode 的源码时,我们发现 vscode 中的依赖注入框架貌似并不是这样消费的。
例如在下面这段鉴权服务中,我们发现该类并没有@injectable()作为类的依赖收集,并且依赖服务也直接用其类名作为修饰器,而不是@inject()。
// srcvsworkbenchservicesauthenticationrowserauthenticationservice.tsexport class authenticationservice extends disposable implements iauthenticationservice {  constructor(    @iactivityservice private readonly activityservice: iactivityservice,    @iextensionservice private readonly extensionservice: iextensionservice,    @istorageservice private readonly storageservice: istorageservice,    @iremoteagentservice private readonly remoteagentservice: iremoteagentservice,    @idialogservice private readonly dialogservice: idialogservice,    @iquickinputservice private readonly quickinputservice: iquickinputservice  ) {}}  
其实这里的修饰符并不是真正指向类名,而是一个同名的资源描述符 id(vscode 中称之为 serviceidentifier),通常使用字符串或 symbol 标识。
通过 serviceidentifier 作为 id,而不是简单粗暴地通过类名称作为 id 注册 service,有利于处理项目中一个 interface 可能存在多态实现,需要同时多个同名类实例的问题。
此外,在构造 serviceidentifier 时,我们便可以将该类声明注入框架,而无需@injectable()显示调用了。
那么,这样一个 serviceidentifier 该如何构造呢?
// srcvsplatforminstantiationcommoninstantiation.ts/** * the *only* valid way to create a {{serviceidentifier}}. */export function createdecorator(serviceid: string): serviceidentifier {  if (_util.serviceids.has(serviceid)) {    return _util.serviceids.get(serviceid)!;  }  const id = 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);  };  id.tostring = () => serviceid;  _util.serviceids.set(serviceid, id);  return id;}// 被 serviceidentifier 装饰的类在运行时,将收集该类的依赖,注入到框架中。function storeservicedependency(id: function, target: function, index: number): void {  if ((target as any)[_util.di_target] === target) {    (target as any)[_util.di_dependencies].push({ id, index });  } else {    (target as any)[_util.di_dependencies] = [{ id, index }];    (target as any)[_util.di_target] = target;  }}  
我们仅需通过createdecorator方法为类创建一个唯一的serviceidentifier,并将其作为修饰符即可。
以上面的 authenticationservice 为例,若所依赖的 activityservice 需要变更多态实现,仅需修改 serviceidentifier 修饰符确定实现方式即可,无需更改业务的调用代码。
export const iactivityserviceplana = createdecorator(iactivityserviceplana);export const iactivityserviceplanb = createdecorator(iactivityserviceplanb);export interface iactivityservice {...}export class authenticationservice {  constructor(    @iactivityserviceplana private readonly activityservice: iactivityservice,  ) {}}  
循环依赖问题
模块之间的依赖关系是有可能存在循环依赖的,比如 a 依赖 b,b 依赖 a。这种情况下进行两个模块的实例化会造成死循环,因此我们需要在框架中加入循环依赖检测机制来进行规避。
本质上,一个健康的模块依赖关系就是一个有向无环图(dag),我们之前介绍过有向无环图在 excel 表格函数中的应用,放在依赖注入框架的设计中也同样适用。
我们可以通过深度优先搜索(dfs)来检测模块之间的依赖关系,如果发现存在循环依赖,则抛出异常。
// src/vs/platform/instantiation/common/instantiationservice.tswhile (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);  }}  
该方法通过获取图节点的出度,将该类的全部依赖提取出来作为roots,然后逐个实例化,并从途中剥离该依赖节点。由于依赖树的构建是逐层依赖的,因此按顺序实例化即可。当发现该类的所有依赖都被实例化后,图中仍存在节点,则认为存在循环依赖,抛出异常。
总结
本篇文章简要介绍并实现了一个依赖注入框架,并解析了vscode在实际问题上做出的一些改进。
实际上 vscode 的依赖注入能力还有很多细节需要处理。例如异步实例化能力支持,通过封装 deferred 类取得promise执行状态,等等,在此就不一一展开了。感兴趣的同学可以参考 vscode 源码:src/vs/platform/instantiation/common/instantiationservice.ts,https://segmentfault.com/a/src/vs/platform/instantiation/common/instantiationservice.ts 做更进一步的学习。


苏州电感厂家科普相同感值尺寸的插件屏蔽电感可以通用吗
微型空气质量监测仪推荐
华为P10曝光:满满的黑科技!你期待吗?
MEMS信号处理电路中的FIFO系统设计
联想小新Air 14 2021锐龙版规格参数曝光
VS Code源码深入浅出--依赖注入设计
厦门光莆电子怎么样 光莆电子股份通过第27批国家企业技术中心认定
一口气带你们认识所有USB标准
简述无线传感器主要类型
京东金融财产安全依托大数据技术获得国际认证
中芯国际宣布将从纽约证券交易所退市
磐石测控:DAGE4000 Optima焊接强度测试机有什么产品详情?
晶振失效会导致开机乱码按键失灵,这个参数你一定要重视
一个采用无线射频识别智能化的医院智慧后勤建设方案
高压变频器在大功率同步电动机上的应用
贾斯汀·汀布莱克与美国运通公司合作开发了一款Outside In的AR应用程序
1n5819二极管参数
AMD新一代Navi GPU曝光,拥有80个计算单元
使用LM3886制作的低音炮,LM3886 Amplifier
如何为工业建筑高棚照明系统选择合适的LED