0
  • 聊天消息
  • 系统消息
  • 评论与回复
登录后你可以
  • 下载海量资料
  • 学习在线课程
  • 观看技术视频
  • 写文章/发帖/加入社区
会员中心
创作中心

完善资料让更多小伙伴认识你,还能领取20积分哦,立即完善>

3天内不再提示

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

jf_8lIj6kO1 来源:SegmentFault思否 作者:Duang 2022-12-14 10:37 次阅读
加入交流群
微信小助手二维码

扫码添加小助手

加入工程师交流群

在阅读 VS Code 代码的过程中,我们会发现每一个模块中都有大量装饰器的使用,用来装饰模块以及其中依赖的模块变量。这样做的目的是什么呢?在这一篇中我们来详细分析一下。

依赖注入介绍


如果有这样一个模块 A,它的实现依赖另一个模块 B 的能力,那么应该如何设计呢?很简单,我们可以在 A 模块的构造函数中实例化模块 B,这样就可以在模块 A 内部使用模块 B 的能力了。

classA{
constructor(){
this.b=newB();
}
}

classB{}

consta=newA();

但是这样做有两个问题,一是模块 A 的实例化过程中,需要手动实例化模块 B,而且如果模块 B 的依赖关系发生变化,那么也需要修改模块 A 的构造函数,导致代码耦合

二是在复杂项目中,我们在实例化模块 A 时,难以判断模块 B 是否被其他模块依赖而已经实例化过了,从而可能将模块 B 多次实例化。若模块 B 较重或者需要为单例设计,这将带来性能问题。

因此,更好的方式是,将所有模块的实例化交给外层框架,由框架统一管理模块的实例化过程,这样就可以解决上述两个问题。

classA{
constructor(privateb:B){
this.b=b;
}
}

classB{}

classC{
constructor(privatea:A,privateb:B){
this.b=b;
}
}

constb=newB();
consta=newA(b);
constc=newC(a,b);

这种将依赖对象通过外部注入,避免在模块内部实例化依赖的方式,称为依赖注入 (Dependencies Inject, 简称 DI)。这在软件工程中是一种常见的设计模式,我们在 Java 的 Spring,JS 的 Angular,Node 的 NestJS 等框架中都可以看到这种设计模式的应用。

当然,在实际应用中,由于模块众多,依赖复杂,我们很难像上面的例子一样,规划出来每个模块的实例化时机,从而编写模块实例化顺序。并且,许多模块可能并不需要第一时间被创建,需要按需实例化,因此,粗暴的统一实例化是不可取的。

因此我们需要一个统一的框架来分析并管理所有模块的实例化过程,这就是依赖注入框架的作用。

借助于 TypeScript 的装饰器能力,VSCode 实现了一个极为轻量化的依赖注入框架。我们可以先来简单实现一下,解开这个巧妙设计的神秘面纱。

最简依赖注入框架设计


实现一个依赖注入框架只需要两步,一个是将模块声明并注册到框架中进行管理,另一个是在模块构造函数中,声明所需要依赖的模块有哪些。

我们先来看模块的注册过程,这需要 TypeScript 的类装饰器能力。我们在注入时,只需要判断模块是否已经注册,如果没有注册,将模块的 id(这里简化为模块 Class 名称)与类型传入即可完成单个模块的注册。

exportfunctionInjectable():ClassDecorator{
return(Target:Class):any=>{
if(!collection.providers.has(Target.name)){
collection.providers.set(Target.name,target);
}
returntarget;
};
}

之后我们再来看看模块是如何声明依赖的,这需要 TypeScript 的属性装饰器能力。我们在注入时,先判断依赖的模块是否已经被实例化,如果没有,则将依赖模块进行实例化,并存入框架中管理。最终返回已经被实例化完成的模块实例。

exportfunctionInject():PropertyDecorator{
return(target:Property,propertyKey:string)=>{

constinstance=collection.dependencies.get(propertyKey);
if(!instance){
constDependencyProvider:Class=collection.providers.get(propertyKey);
collection.dependencies.set(propertyKey,newDependencyProvider());
}

target[propertyKey]=collection.dependencies.get(propertyKey);
};
}

最后只需要保证框架本身在项目运行前完成实例化即可。(在例子中表示为 injector)

exportclassServiceCollection{
readonlyproviders=newMap();
readonlydependencies=newMap();
}

constcollection=newServiceCollection();
exportdefaultcollection;

这样,一个最简化的依赖注入框架就完成了。由于保存了模块的类型与实例,它实现了模块的按需实例化,无需在项目启动时就初始化所有模块。

我们可以尝试调用它,以上面举出的例子为例:

@injectable()
classA{
constructor(@inject()privateb:B){
this.b=b;
}
}

@injectable()
classB{}

classC{
constructor(@inject()privatea:A,@inject()privateb:B){
this.b=b;
}
}

constc=newC();

无需知晓模块 A,B 的实例化时机,直接初始化任何一个模块,框架会自动帮你找到并实例化好所有依赖的模块。

VSCode 的依赖收集实现


上面介绍了一个依赖注入框架的最简实现。但当我们真正阅读 VSCode 的源码时,我们发现 VSCode 中的依赖注入框架貌似并不是这样消费的。

例如在下面这段鉴权服务中,我们发现该类并没有@injectable()作为类的依赖收集,并且依赖服务也直接用其类名作为修饰器,而不是@inject()。

//srcvsworkbenchservicesauthenticationrowserauthenticationService.ts
exportclassAuthenticationServiceextendsDisposableimplementsIAuthenticationService{
constructor(
@IActivityServiceprivatereadonlyactivityService:IActivityService,
@IExtensionServiceprivatereadonlyextensionService:IExtensionService,
@IStorageServiceprivatereadonlystorageService:IStorageService,
@IRemoteAgentServiceprivatereadonlyremoteAgentService:IRemoteAgentService,
@IDialogServiceprivatereadonlydialogService:IDialogService,
@IQuickInputServiceprivatereadonlyquickInputService:IQuickInputService
){}
}

其实这里的修饰符并不是真正指向类名,而是一个同名的资源描述符 id(VSCode 中称之为 ServiceIdentifier),通常使用字符串或 Symbol 标识。

通过 ServiceIdentifier 作为 id,而不是简单粗暴地通过类名称作为 id 注册 Service,有利于处理项目中一个 interface 可能存在多态实现,需要同时多个同名类实例的问题。

此外,在构造 ServiceIdentifier 时,我们便可以将该类声明注入框架,而无需@injectable()显示调用了。

那么,这样一个 ServiceIdentifier 该如何构造呢?

//srcvsplatforminstantiationcommoninstantiation.ts
/**
*The*only*validwaytocreatea{{ServiceIdentifier}}.
*/
exportfunctioncreateDecorator(serviceId:string):ServiceIdentifier{

if(_util.serviceIds.has(serviceId)){
return_util.serviceIds.get(serviceId)!;
}

constid=function(target:Function,key:string,index:number):any{
if(arguments.length!==3){
thrownewError('@IServiceName-decoratorcanonlybeusedtodecorateaparameter');
}
storeServiceDependency(id,target,index);
};

id.toString=()=>serviceId;

_util.serviceIds.set(serviceId,id);
returnid;
}

//被 ServiceIdentifier 装饰的类在运行时,将收集该类的依赖,注入到框架中。
functionstoreServiceDependency(id:Function,target:Function,index:number):void{
if((targetasany)[_util.DI_TARGET]===target){
(targetasany)[_util.DI_DEPENDENCIES].push({id,index});
}else{
(targetasany)[_util.DI_DEPENDENCIES]=[{id,index}];
(targetasany)[_util.DI_TARGET]=target;
}
}

我们仅需通过createDecorator方法为类创建一个唯一的ServiceIdentifier,并将其作为修饰符即可。

以上面的 AuthenticationService 为例,若所依赖的 ActivityService 需要变更多态实现,仅需修改 ServiceIdentifier 修饰符确定实现方式即可,无需更改业务的调用代码。

exportconstIActivityServicePlanA=createDecorator("IActivityServicePlanA");
exportconstIActivityServicePlanB=createDecorator("IActivityServicePlanB");
exportinterfaceIActivityService{...}

exportclassAuthenticationService{
constructor(
@IActivityServicePlanAprivatereadonlyactivityService:IActivityService,
){}
}

循环依赖问题


模块之间的依赖关系是有可能存在循环依赖的,比如 A 依赖 B,B 依赖 A。这种情况下进行两个模块的实例化会造成死循环,因此我们需要在框架中加入循环依赖检测机制来进行规避。

本质上,一个健康的模块依赖关系就是一个有向无环图(DAG),我们之前介绍过有向无环图在 excel 表格函数中的应用,放在依赖注入框架的设计中也同样适用。

我们可以通过深度优先搜索(DFS)来检测模块之间的依赖关系,如果发现存在循环依赖,则抛出异常。

//src/vs/platform/instantiation/common/instantiationService.ts
while(true){
letroots=graph.roots();

//ifthereisnomorerootsbutstill
//nodesinthegraphwehaveacycle
if(roots.length===0){
if(graph.length!==0){
throwCycleError();
}
break;
}

for(letrootofroots){
//createinstanceandoverwritetheservicecollections
constinstance=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做更进一步的学习。


审核编辑 :李倩


声明:本文内容及配图由入驻作者撰写或者入驻合作网站授权转载。文章观点仅代表作者本人,不代表电子发烧友网立场。文章及其配图仅供工程师学习之用,如有内容侵权或者其他违规问题,请联系本站处理。 举报投诉
  • 源码
    +关注

    关注

    8

    文章

    682

    浏览量

    31089
  • 变量
    +关注

    关注

    0

    文章

    615

    浏览量

    29369
  • vscode
    +关注

    关注

    1

    文章

    171

    浏览量

    9001

原文标题:VS Code 源码深入浅出 -- 依赖注入设计

文章出处:【微信号:玩转VS Code,微信公众号:玩转VS Code】欢迎添加关注!文章转载请注明出处。

收藏 人收藏
加入交流群
微信小助手二维码

扫码添加小助手

加入工程师交流群

    评论

    相关推荐
    热点推荐

    VS Code 中`xiaozhi-esp32` 项目文件夹 修改 I2C 设备地址为 `0x78`

    VS Code 中`xiaozhi-esp32` 项目文件夹 修改 I2C 设备地址为 `0x78`
    的头像 发表于 11-28 07:30 523次阅读

    VS Code运行 pytest_hello_world.py

    VS Code运行 pytest_hello_world.py
    的头像 发表于 11-24 00:33 339次阅读

    Joycode 无法跨项目读取源码怎么办?MCP Easy Code Reader 帮你解决!

    Code Agent 帮我们分析逻辑和编写代码,而无需再手动将源码复制到对话框中发送给 AI,提高 Code Agent 准确度和编码效率。MCP 已发布 Github: easy-code
    的头像 发表于 11-19 15:50 847次阅读
    Joycode 无法跨项目读取<b class='flag-5'>源码</b>怎么办?MCP Easy <b class='flag-5'>Code</b> Reader 帮你解决!

    如何在VS code中配置Zephyr集成开发环境

    上一篇文章介绍了如何在VS code中使用瑞萨官方插件为RA芯片创建项目与项目调试,相信大家对RA在VS code中的开发有了基础的了解。
    的头像 发表于 11-05 14:46 1065次阅读
    如何在<b class='flag-5'>VS</b> <b class='flag-5'>code</b>中配置Zephyr集成开发环境

    SEGGER工具链集成到CMake和VS Code

    SEGGER公司已将其嵌入式开发工具链集成到了广泛使用的CMake构建配置工具中,这意味着基于Visual Studio CodeVS Code)代码编辑器的应用开发可以方便的使用SEGGER工具实现了。
    的头像 发表于 07-23 15:06 775次阅读

    深入浅出解析:为什么高精度测量要选择12bit示波器?

    示波器是电子工程师的核心工具,能够直观观察信号特性。本文通过对比实验,分析12bit和8bit示波器在信号捕捉和波形还原方面的关键差异,解释为何高精度测量工作中,12bit示波器能提供更可靠的测量结果。8bit与12bit:数字背后的真相什么是示波器的“bit数”时?简单来说,这是示波器ADC(模数转换器)的分辨能力,决定了仪器能够识别的最小电压变化。这个看
    的头像 发表于 05-22 11:40 788次阅读
    <b class='flag-5'>深入浅出</b>解析:为什么高精度测量要选择12bit示波器?

    程序设计与数据结构

    的地址)出发,采用推导的方式,深入浅出的分析了广大C程序员学习和开发中遇到的难点。 2. 从方法论的高度对C语言在数据结构和算法方面的应用进行了深入讲解和阐述。 3. 讲解了绝大多数C程序员开发
    发表于 05-13 16:45

    如何在VS Code中使用瑞萨RA系列MCU

    VS Code(Visual Studio Code)是微软公司出品,它是一个免费且多功能的代码编辑器,几乎支持所有主要的编程语言和框架。特别是最近又新加了Github Copilot功能,让用户
    的头像 发表于 04-16 14:02 3289次阅读
    如何在<b class='flag-5'>VS</b> <b class='flag-5'>Code</b>中使用瑞萨RA系列MCU

    全面解析新概念模拟电路(建议下载!)

    全文共五册,近50万字,一样的风趣幽默,一样的social化语言,深入浅出地将枯燥深奥的模电知识讲得简单易学。 《新概念模拟电路》内容包含了《晶体管》、《负反馈和运算放大器》、《运放电路的频率特性
    发表于 04-16 13:37

    深入浅出解析低功耗蓝牙协议栈

    深入Bluetooth LE协议栈各个组成部分之前,我们先看一下Bluetooth LE协议栈整体架构。 如上图所述,要实现一个Bluetooth LE应用,首先需要一个支持Bluetooth
    的头像 发表于 04-09 14:49 1006次阅读
    <b class='flag-5'>深入浅出</b>解析低功耗蓝牙协议栈

    2025 中国华东智能家居创新技术研讨会现场直击,高精度算法如何改变生活?--其利天下

    在此次展会上,我司技术总监冯建武先生带来了《智能家居的 “心脏” 革命:高精度电机驱动算法如何重塑未来生活》的演讲,深入浅出地阐述了我司目前在无刷马达自适应算法、FOC控制算法等技术上的优势,以及在智能家居领域上的发展战略。
    的头像 发表于 03-30 11:11 828次阅读
    2025 中国华东智能家居创新技术研讨会现场直击,高精度算法如何改变生活?--其利天下

    《零基础开发AI Agent——手把手教你用扣子做智能体》

    《零基础开发AI Agent——手把手教你用扣子做智能体》是一本为普通人量身打造的AI开发指南。它不仅深入浅出地讲解了Agent的概念和发展,还通过详细的工具介绍和实战案例,帮助读者快速掌握
    发表于 03-18 12:03

    深居AutoCAD二次开发

    深居AutoCAD二次开发,net版
    发表于 01-06 14:12 12次下载

    使用MCUXpresso for VS Code插件开发Zephyr的hello world

    本期来到Zephyr实战经验演练,小编带着大家一起使用MCUXpresso for VS Code插件来开发一个属于Zephyr的hello world。
    的头像 发表于 01-03 09:21 1780次阅读
    使用MCUXpresso for <b class='flag-5'>VS</b> <b class='flag-5'>Code</b>插件开发Zephyr的hello world

    Zephyr领进门系列:MCUXPresso for VS Code插件安装

    在上一期-Zephyr的构建工具,我们为大家介绍了一位新朋友,Zephyr OS。相信通过上一篇的介绍,大家已经对这一OS有了一些简单的了解。那么本期小编将带着大家一起从0开始结合VS Code搭建
    的头像 发表于 12-19 09:53 2738次阅读
    Zephyr领进门系列:MCUXPresso for <b class='flag-5'>VS</b> <b class='flag-5'>Code</b>插件安装