隐藏具体实现上次(链接:vscode初探)我们分析了vscode的基本结构和主要模块的作用。如果有同学看过它的源码,会发现要定位到某个模块的具体实现并不容易。这是因为vscode采用了低耦合的模块架构,即在编写某个类的过程中,你所依赖的类被有意隐藏起来。站在架构师的角度,希望每个开发人员都专注于自己负责的模块,不要去关心和理解别人写的模块(减少心理成本),但是如果你的模块依赖了怎么办对另一个同学??答案是通过接口来提供的,这也是面向对象编程的核心。模块依赖于接口并屏蔽特定的实现。这样做的好处是模块之间的耦合度低。这个比较抽象。让我们举个例子。你写的模块A依赖了小明写的一个打印功能模块B,架构师说不能直接调用它的代码。你必须让小明实现接口P,然后你引入接口JustP。那么当架构师再让代码跑起来的时候,小明的打印代码就会自动调用。后来小明负责的打印机被淘汰了,让小王实现了一种新型的打印机功能模块C,你要和小王对接,架构师说,不要和他沟通,小王的打印模块可以实现我们之前约定的接口P,你的代码不需要调整,系统会在运行时自动调用小王编写模块C。接口P的实现可以是B也可以是C。这个行为是也叫多态(面向对象中的子类多态)。在大多数语言中,如c#、java等,都提供了这样的概念,有的称为接口,有的称为抽象类。这种屏蔽具体实现的概念在前端开发领域也被称为“鸭子式”。熟悉vscode的前提是要有面向对象的思维。大量模块如何维护在vscode源码中,模块数以万计。在多人协作和反复迭代的过程中,vscode团队是如何有效管理这些对象的?答案就是DI(DependencyInjection),理解它是深入vscode的核心。JavaSpring、Angular框架也有类似的概念,但在大多数前端开发中并不常见。所以在正式接触vscode本身实现的DI之前,先一步步介绍一下为什么需要DI,它解决了哪些痛点,它做了什么。举个简单的例子,实际情况会更复杂。下面的类代表一个类。在大多数场景下,它也可以称为模块。类初始化后,我们称它为实例或对象。后面的名词就不区分了。case:class1依赖:2,3,4class2依赖:5,6,class3依赖:7,8class4依赖:5,7class7依赖:11,12calss6依赖:...等等,在系统,modules依赖程度可能非常深。我们编写上面的模型(伪代码)//class1dependson2,3,4importClass5fromClass5importClass7fromClass7importClass2fromClass2importClass3fromClass3importClass4fromClass4classClass1{constructor(){//commomstartcommondependencyonthis.instance5=新类5();this.instance7=newClass7();//公共发送this.instance2=newClass2(this.instance5);this.instance3=newClass3(this.instance7);this.instance4=newClass4(this.instance5,this.instance7)}}//class2依赖5,6importClass8fromClass6classClass2{constructor(instance5){this.instance5=instance5;this.instance6=newClass6();}}//class3依赖7,8importClass8fromClass8classClass3{constructor(instance7){this.instance7=instance7;this.instance8=newClass8();}}//class4依赖于5,7importClass5fromClass5importClass7fromClass7classClass4{constructor(){this.instance7=newClass5();this.instance8=newClass7();}}//类7依赖于11、12importClass11fromClass11importClass12fromClass12classClass7{constructor(){this.instance11=newClass11();this.instance12=newClass12();通常在简单的场景下,我们会直接在类内部新建一个类,作为依赖,挂载在属性成员classClass7下{constructor(){this.instance11=newClass11();//...参考上面的代码}}如上,我们在class7中新建了Class11,他们之间的关系是强依赖关系。即7类在内部引用11类的源代码,同时控制11类的初始化过程。随着迭代,这种依赖性将变得无法维护。比如某天Class11的实现发生了变化,Class11需要换成其他的类,我们还是要在class7等类似的模块中修改。共同依赖假设instance11需要同时被其他classX需要?我们可以将创建的instance11作为参数传递给多个依赖类X。classClass1{/**公共依赖5class2->instance5class4->instance5公共依赖7class3->instance7class4->instance7*/constructor(){//外部初始化//commostart公共依赖this.instance5=newClass5();this.instance7=newClass7();//公共发送this.instance2=newClass2(this.instance5);this.instance3=newClass3(this.instance7);this.instance4=newClass4(this.instance5,this.instance7)}}在上面的伪代码中,instance5和instance7是分别依赖于class2和class3两个类的实例,所以我们找到class2和classclass1class3共同向上依赖,即提前在外部初始化instance5和instance7,作为参数传递给class2和class3。这样,你会发现随着应用程序变得越来越复杂,你将需要小心。我创建的类所依赖的实例是否可以在类构造函数中直接new?如果同时依赖其他类,我需要去哪里找这些类,又要在哪里初始化这些实例呢?循环依赖另外,在逐渐增加依赖的过程中,非常容易出现循环依赖的问题。class2依赖class7,class7依赖class11。假设在class11中有一个依赖class2的开发,那么就变成了依赖循环依赖。如果在构建过程中没有检测到循环依赖,程序运行时就会出现空引用等bug。可以想象,在一个有N*M个依赖的复杂应用中,单纯开发和手动维护类之间的关系的成本是非常高的。在数千个模块中,每个人只写一个小模块。这种情况下,稍微不小心改动一个点,就会引发灾难性的雪崩,而如此强的依赖,后期基本无法维持。去除源码依赖为了让每个类不依赖其他类的源码,我们可以将所有实例的初始化放在类之外,就像处理公共依赖一样。比如一个入口函数,主类等。在上面的例子中,我们可以这样调整。classClass1{constructor(){//入口函数中的所有初始化//需要依次声明上中下层的依赖this.instance11=newClass11();this.instance12=newClass12();this.instance5=newClass5();//直接将依赖示例传入this.instance7=newClass7(this.instance11,this.instance12);this.instance2=newClass2(this.instance5);this.instance3=newClass3(this.instance7);this.instance4=newClass4(this.instance5,this.instance7)}}//其他类代码省略...//每个类不需要在源码层面引入依赖,比如import语句初始化最外层依赖的好处是,原本需要在源代码层面通过import(include/useing)等声明来引入依赖,现在变成了将依赖作为运行时实例作为参数传入。现在,我们有进一步的可维护性。对于每个模块的使用者来说,不再需要关心依赖模块的源代码在哪里。相反,它接受参数并可以直接使用。但是这种情况还有一个问题:对于负责在外层初始化模块的同学来说,他必须了解整个项目的所有模块的细节,分析依赖顺序,然后在一个合适的地方。这是一项大量的体力劳动,而且很容易出错。面对循环依赖等问题,也需要仔细检查。每次改代码,都少不了这位“大神”的参与。如果有不符合要求的模块,让负责人修改。.假设某天“大神”放假,项目可能会很糟糕。像依赖注入这样的工作可以自动化吗?DI(依赖注入)框架就是这样做的。也就是说,在大型项目中,我们需要有这样一种机制:1.模块之间没有源码依赖(这里的模块主要是指类)2.只依赖接口/抽象,不依赖具体的实现3.模块的创建、循环引用、错误等都可以自动捕获到最后。我们可以看到上图所示的效果。原始Class2依赖于Class5源代码级别。就变成了Class2依赖InterfaceClass5,Class5负责实现InterfaceClass5接口。这样的关系/原理,我们称之为依赖倒置(DI只是一种具体的实现),可以看到Class2直接向下控制Class5的方向,变成Class5来实现InterfaceClass5接口,该接口是一个中间桥梁,Class2和class5方向相反。(不知道通俗不通俗,结合上图箭头方向就可以理解)下面是最后的代码示例:classClass2{constructor(@aotuInjectpriinstance5:InterfaceClass5,@aotuInjectinstance6:InterfaceClass6){this.instance5=instance5;这个.instance6=instance6;}}Class2只需要关心InterfaceClass5暴露的是什么接口,具体的实现却被刻意屏蔽了。这个过程由DI框架自动处理。至此,我们简单说明了在vscode设计过程中通过接口抽象和多态性思考的重要性,这是可维护性的前提。然后通过一个例子来介绍依赖注入可以解决哪些问题,需要做什么。下一步,我将详细介绍DI在vscode中的实现细节。预告下(剧透),typescript装饰器的使用,以及一致的接口名称和装饰描述符的巧妙设计,非常惊艳,源码也就几百行左右。————————文档信息标题:vscode解析——如何维护海量模块依赖(一)发布日期:2022-05-09笔名:ChaosFuwang
