找回密码
 会员注册
查看: 12|回复: 0

如何利用IOC改善工程设计

[复制链接]

3

主题

0

回帖

10

积分

新手上路

积分
10
发表于 2024-9-30 23:30:13 | 显示全部楼层 |阅读模式
控制反转 (Inversion of Control) 及其背后的 SOLID 设计原则已经非常成熟,并且在传统软件开发领域得到了验证。本文从 JavaScript 生态出发,结合领域内流行的基础设施和成功的项目样例,对这套这套方法论进行重新审视。绪论什么是 IOC 控制反转一个来自 React 的例子:Context – React[1]Avatar 虽然被诸多底层组件依赖,但是它却不是被底层组件引入并初始化的,这样就实现了底层组件与 Avatar 的解耦。底层组件不再关注 Avatar 的具体实现仅关注一个抽象的承诺:上层组件会传入一个可渲染的片段Avatar 的初始化由多个地点集中到了一起完成了复杂度的收束,并且没有影响代码的能力。这个例子仅说明了 IOC 的核心,完整的 IOC 实践与 SOLID 设计原则 紧密相关维基百科: SOLID (面向对象设计)实际上这也是 IOC 难以被讲透的主要原因:IOC 不是一种单独的技术,而是一整套方法论。这套方法论试图解决从项目架构设计,开发协作流程,再到后期项目迭代直到代码老化等多个环节中的多个问题。很难说某些优势是不是 IOC 直接带来的,但是 IOC 确实和这套方法论配合良好,后面可以看到例子。两个关键点:单一功能原则 确保了功能单元的可复用性,同时带来了一些好处。边界清晰,关注点集中;代码即文档,降低命名难度,有助于提升可读性。单元间的调用基于 interface 的共识。也被称为 Interface Driven。在设计之初,自顶向下地拆分功能模块,并明确各单元间的接口(在依赖单元被实现之前,不阻塞当前单元被开发,有利于团队协作与并行);对其他模块的认知仅限于 interface,而不应依赖其特定的实现方式(可替换性:便于 Mock 和 重构)。模块与 IOC在社区中,也有一些声音认为借助模块系统的能力,JavaScript 可以获取与 IOC 类似的优势。举个例子://my-class.tsClassMyClass{}//单例exportconstmyClass=newMyClass();//工厂函数exportconstmakeMyClass(){returnnewMyclass();}//foo.tsimport{myClass}from'my-class.ts';//bar.tsimport{makeMyClass}from'my-class.ts';这里的 myClass 可以是单例的,并且在它自己的模块中被初始化,其它模块不需要知道细节。在小型项目中,这样处理是足够好的,简单且符合直觉,但是:实际上发生了耦合,对 my-class.ts 的引用就是这种耦合的体现。在一个非常大的项目 Repo 中,需要关注 my-class.ts 的文件位置,它甚至可能位于另一个 Package。依赖了一个具体的实现而非接口。因此在你编写这段代码的时候,MyClass 需要存在,并且实现了你需要的接口。这种依赖缺乏某种预先设计,非常不利于协作。潜在的循环依赖问题。Modules: CommonJS modules | Node.js v19.4.0 Documentation[2]ES6 Modules and Circular Dependency[3]myClass 单例的生命周期是不可控的,被实例化的时机是不明确的。工厂函数需要专门编写。InversifyJS:JavaScript 生态内最流行的 IOC 框架InversifyJS 是一个轻量的 (4KB) IOC 容器 ,可用于编写 TypeScript 和 JavaScript 应用。主要目标:允许 JavaScript 开发人员编写遵循 SOLID 原则的代码。促进并鼓励遵守最佳的面向对象编程和依赖注入实践。尽可能少的运行时开销。提供艺术编程体验和生态。一分钟认识 InversifyJS提供一个容器的基础设施,各模块都被注册到 Container 容器中。容器可以简单理解为一个 Map:container = new Container()容器中的每个单元拥有自己的 标识符(Service Identifier) 和预先定义的 interface。1、标识符是集中声明的常量,其值一般是一个 Symbol 对象,例如下文中的 TYPES.FOO2、interface 使用 TS 声明,例如下文中的Foo3、标识符 和 interface 共同构成了在设计阶段的 模块抽象模块注册到容器是集中完成的1、FooImpl 实现了 Foo 接口,以下代码将其实现与抽象绑定2、container.bind(TYPES.FOO).to(FooImpl)当其它模块需要与模块 Foo 交互,通过 @inject(标识符) 声明对 Foo 的依赖,Foo 的实例会被自动注入。@inject(TYPES.FOO)标识符(Service Identifier) 也可以使用 string 或者其它类型,只要意义清晰即可。其 TS 声明如下:InversifyJS 实战本小节基于官方文档改编步骤 1: 声明接口和类型目标是编写遵循依赖倒置原则的代码,这意味着我们应该“依赖于抽象而不依赖于具体实现”。先声明一些 interface://fileinterfaces.tsinterfaceWarrior{fight():string;sneak():string;}interfaceWeapon{hit():string;}interfaceThrowableWeapon{throw():string;}Inversifyjs 需要在运行时使用类型标记作为标识符。接下来将使用 Symbol 作为标识符://filetypes.tsconstTYPES={Warrior:Symbol.for("Warrior"),Weapon:Symbol.for("Weapon"),ThrowableWeapon:Symbol.for("ThrowableWeapon")};export{TYPES};这一步完成了整个应用的 模块抽象 设计。步骤 2: 使用 @injectable 和 @inject 装饰器声明依赖编写一些类,来实现上一步声明的 interface。希望使用依赖注入的类需要添加 @injectable 装饰器来激活这个特性,然后就可以使用@inject 声明依赖。//fileentities.tsimport{injectable,inject}from"inversify";import"reflect-metadata";import{Weapon,ThrowableWeapon,Warrior}from"./interfaces"import{TYPES}from"./types";@injectable()classKatanaimplementsWeapon{publichit(){return"cut!";}}@injectable()classShurikenimplementsThrowableWeapon{publicthrow(){return"hit!";}}@injectable()classNinjaimplementsWarrior{private_katana:Weapon;private_shuriken:ThrowableWeapon;publicconstructor(@inject(TYPES.Weapon)katana:Weapon,@inject(TYPES.ThrowableWeapon)shuriken:ThrowableWeapon){this._katana=katana;this._shuriken=shuriken;}publicfight(){returnthis._katana.hit();}publicsneak(){returnthis._shuriken.throw();}}export{Ninja,Katana,Shuriken};可选地,也支持使用属性注入来代替构造函数注入,更加简洁:@injectable()classNinjaimplementsWarrior{@inject(TYPES.Weapon)private_katana:Weapon;@inject(TYPES.ThrowableWeapon)private_shuriken:ThrowableWeapon;publicfight(){returnthis._katana.hit();}publicsneak(){returnthis._shuriken.throw();}}步骤 3: 创建和配置容器这一步骤我们真正将 实现 绑定到各自的 抽象 上。推荐在命名为 inversify.config.ts 的文件中创建和配置容器。这是唯一有耦合的地方,项目的其它地方,不应该包含对其他类的引用。//fileinversify.config.tsimport{Container}from"inversify";import{TYPES}from"./types";import{Warrior,Weapon,ThrowableWeapon}from"./interfaces";import{Ninja,Katana,Shuriken}from"./entities";constmyContainer=newContainer();myContainer.bind(TYPES.Warrior).to(Ninja);myContainer.bind(TYPES.Weapon).to(Katana);myContainer.bind(TYPES.ThrowableWeapon).to(Shuriken);export{myContainer};步骤 4: 解析依赖您可以使用方法 get从 Container 中获得依赖。应该在根结构(尽可能靠近应用程序的入口点的位置)去解析依赖(指引入 inversify.config),避免反模式的服务定位器问题。译文:服务定位器 Service Locator 是一种反模式的设计[4]import{myContainer}from"./inversify.config";import{TYPES}from"./types";import{Warrior}from"./interfaces";constninja=myContainer.get(TYPES.Warrior);expect(ninja.fight()).eql("cut!");//trueexpect(ninja.sneak()).eql("hit!");//trueInversifyJS 的优势本小节基于官方文档改编解耦与依赖抽象InversifyJS 赋予你真正解耦的能力。在上一小节的实战中,Ninja 类永远不会直接持有 Katana 或者 Shuriken 类。但是,它会指向接口(在设计时)或者符号(在运行时)。由于这是抽象的所以这是可接受的。毕竟 依赖抽象 正是依赖反转所要做的。InversifyJS 容器是应用中唯一清楚生命周期和依赖关系的元素。应用中所有的耦合关系发生在唯一一处:inversify.config.ts 文件中。这非常重要,想象我们正在更改一个游戏的难度级别,只需要去 inversify.config.ts 文件中并且修改 Katana 的绑定即可:import{Katana}from"./entitites/SharpKatana";if(difficulty==="hard"){container.bind(TYPES.KATANA).to(SharpKatana);}else{container.bind(TYPES.KATANA).to(Katana);}你根本不需要修改 Ninja 文件!想象一下,如果你在 inversify.config 当中实现一些小机制,理论上可以在运行时对应用的所有功能单元进行动态替换,然后你得到了一个所有内部单元都可以做 AB 测试/灰度发布应用!在下一小节 Theia 的架构中,可以看到此机制是如何提供了魔法般的高度的可定制性与灵活性。需要付出的代价是符号或者字符串字面量的使用,但是只要你在一个文件中定义所有的字符串字面量,那么这个代价将有所缓和 (Redux 中的 actions 就是这么做的)。好消息是未来这些符号或者字符串字面量能够由 TS 编译器自动生成,但是目前这还在 TC39 委员会的手中。解决对象组合的痛点一个常见的模式:varsvc=newShippingService(newProductlocator(),newPricingService(),newInventoryService(),newTrackingRepository(newConfigProvider()),newLogger(newEmailLogger(newConfigProvider())));单元之间层层嵌套的依赖关系是 OOP 的一个痛点,并且这种嵌套关系会很快增长到无法有效维护。即使使用工厂函数,你所编写的额外代码仍然是不划算的。类型安全支持 TypeScript ,被注入的模块有完整的类型声明高级特性解决复杂依赖关系可选依赖:@optional() 装饰器声明一个可选依赖层次化的容器1、可以将多个 Container 使用类似原型链的方式嵌套连接,其寻址方式也类似原型链2、childContainer.parent = parentContainer多重注入1、当有两个或者多个具体实现被绑定到同一个标识符,可以使用多重注入2、@multiInject 装饰器会将多个实现以数组方式注入解决循环依赖1、@lazyInject 装饰器将对依赖项的注入延迟到了真正要使用它们的那一刻,这发生在类实例被创建之后2、有能力识别循环依赖,并且会给出提示信息中间件与拦截器:Logger容器内容的生命周期管理:单元被绑定时可以声明其生命周期TransientScope 默认值,每次从容器中获取时都初始化新实例SingletonScope 单例,每次获取返回同一实例RequestScope 前两者的混合,在同一个依赖树上总是返回同一实例开发者工具Dive Into TheiaEclipse Theia [5]是一个使用现代 Web 技术构建自定义云和桌面 IDE 和工具的平台。Theia 本身并不是一个工具,Theia 是一个开发 IDE 的框架,可以基于 Theia 创建自己的 IDE。Theia 使用 Typescript 编写,整体技术体系和 Visual Studio Code 类似。Theia 为什么是一个好例子出身名门高完成度足够复杂挑战大代码量多以开源项目方式维护足够新使用现代技术栈基于 TypeScript 的 IOC & SOLID 实践Theia 的目标与挑战多平台:整个应用可运行于 B/S 模式,也可运行于 Electron 中对标 VS Code 的现代 IDE 架构,兼容 VS Code 插件高可维护性的模块化架构尽可能复用基础功能使用标准组件,不重复造轮子高扩展性与灵活性本质上是个框架,设计出来就是为了二次开发用户可以轻松改变、扩展内置模块的行为用户可以按照规约添加新的模块和功能这几个目标对于应用架构设计提出了极高的要求。Theia 的架构设计Theia 整体上分为前端和后端两个子应用,中间使用 JSON-RPC 通信。前端负责显示 UI,处理交互,运行在浏览器(或 Electron 窗口)中。前端进程启动时,将首先加载所有 Extension 贡献的 DI 模块,然后获取 FrontendApplication 的实例并在其上调用 start()。后端运行在 Node.js 中,是一个基于 Express.js 的服务。后端应用程序的启动会首先加载所有所有 Extension 贡献的 DI 模块,然后它会获取一个 BackendApplication 实例并在其上调用 start(portNumber)。依赖注入前后端都使用 DI(具体来说就是 Inversify.js)来组合逻辑,稍后我们会详细讨论。ExtensionExtension 是 Theia 中的功能模块(npm package),Theia 就是由无数个 Extension 组成的。编写一个 Extension 是用户定制 Theia 的主要方式。用户提供的 Extension 会和 Theia 内置的 Extension 一起经历编译过程,并产出一个可运行的应用。用户 Extension 和内置 Extension 地位相同,其权限和能力几乎不受限制。注意这与 VS Code 定义的插件(VS Code Extension)是不同的。插件是运行时可动态加载的,在 Theia 中被称为 Plugin。因为 Theia 由前后端两个子应用组成,所以 Extension 一般也由前后端两部分组成,其典型目录结构为:common 目录包含不依赖于任何运行时的代码一般包含前后端 RPC 接口的定义,常量,通用的工具函数等browser 目录包含需要现代浏览器作为平台 (DOM API) 的代码。electron-browser 目录包含需要 DOM API 以及 Electron renderer-process 特定 API 的前端代码。node 目录包含需要 Node.js 作为平台的(后端)代码。node-electron 目录包含专用于 Electron 的(后端)代码。可以通过 Theia 的内置模块[6],来一窥其是怎么进行模块划分的。扁平且清晰。构建 Theia 应用Theia 可以基于 Package.json 声明构建:{"private":true,"dependencies":{"@theia/callhierarchy":"latest","@theia/console":"latest","@theia/core":"latest","@theia/debug":"latest","@theia/editor":"latest","@theia/editor-preview":"latest","@theia/file-search":"latest","@theia/filesystem":"latest","@theia/getting-started":"latest",//以下省略},"devDependencies":{"@theia/cli":"latest"},"scripts":{"preinstall":"node-gypinstall"}}通过编辑 dependencies, 可以挑选本次构建包含哪些功能模块。拆解一个 Theia Extension此处以内置 Package file-search 为例,探索一下其内部实现,这个模块实现了弹出式文件选择弹窗:其目录结构如下:Commoncommon/file-search-service.ts前后端 JSON-RPC 接口定义标识符 Symbol 定义本模块的 inerface其它常量定义import{CancellationToken}from'@theia/core';exportconstfileSearchServicePath='/services/search';/***TheJSON-RPCfilesearchserviceinterface.*/exportinterfaceFileSearchService{/***findsfilesbyagivensearchpattern.*@returnthematchingfileuris*/find(searchPattern:string,options:FileSearchService.Options,cancellationToken:CancellationToken)romise;}exportconstFileSearchService=Symbol('FileSearchService');exportnamespaceFileSearchService{exportinterfaceBaseOptions{useGitIgnore:booleanincludePatterns:string[]excludePatterns:string[]}exportinterfaceRootOptions{[rootUri:string]:BaseOptions}exportinterfaceOptionsextendsBaseOptions{rootUris:string[]rootOptions:RootOptionsfuzzyMatch:booleanlimit:number}}exportconstWHITESPACE_QUERY_SEPARATOR=/\s+/;后端node/file-search-service-impl.ts这里实现了功能的后端服务,依赖的模块使用 @inject 注入:import*ascpfrom'child_process';import*asreadlinefrom'readline';import{injectable,inject}from'@theia/core/shared/inversify';importURIfrom'@theia/core/lib/common/uri';import{FileUri}from'@theia/core/lib/node/file-uri';import{RawProcessFactory}from'@theia/process/lib/node';import{FileSearchService,WHITESPACE_QUERY_SEPARATOR}from'../common/file-search-service';import*aspathfrom'path';@injectable()exportclassFileSearchServiceImplimplementsFileSearchService{constructor(@inject(ILogger)protectedreadonlylogger:ILogger,/**@deprecatedsince1.7.0*/@inject(RawProcessFactory)protectedreadonlyrawProcessFactory:RawProcessFactory,){}asyncfind(searchPattern:string,options:FileSearchService.Options,clientToken:CancellationToken)romise{//略去具体实现}privatedoFind(rootUri:URI,options:FileSearchService.BaseOptions,acceptfileUri:string)=>void,token:CancellationToken)romise{//略去具体实现}privategetSearchArgs(options:FileSearchService.BaseOptions):string[]{//略去具体实现}}node/file-search-backend-module.ts类似 inversify.config.ts 的作用,将 FileSearchServiceImpl 和 ConnectionHandler 绑定到其抽象:import{ContainerModule}from'@theia/core/shared/inversify';import{ConnectionHandler,JsonRpcConnectionHandler}from'@theia/core/lib/common';import{FileSearchServiceImpl}from'./file-search-service-impl';import{fileSearchServicePath,FileSearchService}from'../common/file-search-service';exportdefaultnewContainerModule(bind=>{bind(FileSearchService).to(FileSearchServiceImpl).inSingletonScope();bind(ConnectionHandler).toDynamicValue(ctx=>newJsonRpcConnectionHandler(fileSearchServicePath,()=>ctx.container.get(FileSearchService))).inSingletonScope();});前端browser/quick-file-open.ts包含 UI 相关的主要业务逻辑:import{inject,injectable,optional,postConstruct}from'@theia/core/shared/inversify';import{OpenerService,KeybindingRegistry,QuickAccessRegistry,QuickAccessProvider,CommonCommands}from'@theia/core/lib/browser';import{WorkspaceService}from'@theia/workspace/lib/browser/workspace-service';importURIfrom'@theia/core/lib/common/uri';import{FileSearchService,WHITESPACE_QUERY_SEPARATOR}from'../common/file-search-service';import{CancellationToken,Command,nls}from'@theia/core/lib/common';import{LabelProvider}from'@theia/core/lib/browser/label-provider';import{NavigationLocationService}from'@theia/editor/lib/browser/navigation/navigation-location-service';import*asfuzzyfrom'@theia/core/shared/fuzzy';import{MessageService}from'@theia/core/lib/common/message-service';import{FileSystemPreferences}from'@theia/filesystem/lib/browser';import{EditorOpenerOptions,EditorWidget,Position,Range}from'@theia/editor/lib/browser';import{findMatches,QuickInputService,QuickPickItem,QuickPicks}from'@theia/core/lib/browser/quick-input/quick-input-service';exportconstquickFileOpen=Command.toDefaultLocalizedCommand({id:'file-search.openFile',category:CommonCommands.FILE_CATEGORY,label:'OpenFile...'});exportinterfaceFilterAndRange{filter:string;range:Range;}//Supportspatternsof constLINE_COLON_PATTERN=/\s[#](:line"#")(\d*)(:[#:,](\d*"#:,")))\s*$/;exporttypeFileQuickPickItem=QuickPickItem&{uri:URI};@injectable()exportclassQuickFileOpenServiceimplementsQuickAccessProvider{staticreadonlyPREFIX='';@inject(KeybindingRegistry)protectedreadonlykeybindingRegistry:KeybindingRegistry;@inject(WorkspaceService)protectedreadonlyworkspaceService:WorkspaceService;@inject(OpenerService)protectedreadonlyopenerService:OpenerService;@inject(QuickInputService)@optional()protectedreadonlyquickInputServiceuickInputService;@inject(QuickAccessRegistry)protectedreadonlyquickAccessRegistryuickAccessRegistry;@inject(FileSearchService)protectedreadonlyfileSearchService:FileSearchService;@inject(LabelProvider)protectedreadonlylabelProviderabelProvider;@inject(NavigationLocationService)protectedreadonlynavigationLocationService:NavigationLocationService;@inject(MessageService)protectedreadonlymessageService:MessageService;@inject(FileSystemPreferences)protectedreadonlyfsPreferences:FileSystemPreferences;registerQuickAccessProvider():void{this.quickAccessRegistry.registerQuickAccessProvider({getInstance)=>this,prefixuickFileOpenService.PREFIX,placeholder:this.getPlaceHolder(),helpEntries:[{description:'OpenFile',needsEditor:false}]});}/***Whethertohide.gitignored(andotherignored)files.*/protectedhideIgnoredFiles=true;/***Whetherthedialogiscurrentlyopen.*/protectedisOpen=false;privateupdateIsOpen=true;protectedfilterAndRangeDefault={filter:'',range:undefined};/***Trackstheuserfilesearchfilterandlocationrangee.g.fileFilter:line:columnorfileFilter:line,column*/protectedfilterAndRange:FilterAndRange=this.filterAndRangeDefault;/***Thescoreconstantswhencomparingfilesearchresults.*/privatestaticreadonlyScores={max:1000,//representsthemaximumscorefromfuzzymatching(Infinity).exact:500,//representsthescoreassignedtoexactmatching.partial:250//representsthescoreassignedtopartialmatching.};@postConstruct()protectedinit():void{//省略}isEnabled():boolean{returnthis.workspaceService.opened;}open():void{//省略}}browser/quick-file-open-contribution.ts注册菜单,快捷键和 Command,实现触发时的回调:import{injectable,inject}from'@theia/core/shared/inversify';importURIfrom'@theia/core/lib/common/uri';import{QuickFileOpenService,quickFileOpen}from'./quick-file-open';import{CommandRegistry,CommandContribution,MenuContribution,MenuModelRegistry}from'@theia/core/lib/common';import{KeybindingRegistry,KeybindingContribution,QuickAccessContribution}from'@theia/core/lib/browser';import{EditorMainMenu}from'@theia/editor/lib/browser';import{nls}from'@theia/core/lib/common/nls';@injectable()exportclassQuickFileOpenFrontendContributionimplementsQuickAccessContribution,CommandContribution,KeybindingContribution,MenuContribution{@inject(QuickFileOpenService)protectedreadonlyquickFileOpenServiceuickFileOpenService;registerCommands(commands:CommandRegistry):void{commands.registerCommand(quickFileOpen,{//eslint-disable-next-line@typescript-eslint/no-explicit-anyexecute...args:any[])=>{letfileURI:string|undefined;if(args){[fileURI]=args;}if(fileURI){this.quickFileOpenService.openFile(newURI(fileURI));}else{this.quickFileOpenService.open();}}});}registerKeybindings(keybindings:KeybindingRegistry):void{keybindings.registerKeybinding({command:quickFileOpen.id,keybinding:'ctrlcmd+p'});}registerMenus(menus:MenuModelRegistry):void{menus.registerMenuAction(EditorMainMenu.WORKSPACE_GROUP,{commandId:quickFileOpen.id,label:nls.localizeByDefault('GotoFile...'),order:'1',});}registerQuickAccessProvider():void{this.quickFileOpenService.registerQuickAccessProvider();}}browser/file-search-frontend-module.ts与后端类似,完成实现到抽象的绑定。通过 RPC 调用后端服务,实际上是一个透明的 Proxy;上文中 QuickFileOpenFrontendContribution 分别实现了 QuickAccessContribution, CommandContribution等多个 interface,所以这里分别完成绑定。import{ContainerModule,interfaces}from'@theia/core/shared/inversify';import{CommandContribution,MenuContribution}from'@theia/core/lib/common';import{WebSocketConnectionProvider,KeybindingContribution}from'@theia/core/lib/browser';import{QuickFileOpenFrontendContribution}from'./quick-file-open-contribution';import{QuickFileOpenService}from'./quick-file-open';import{fileSearchServicePath,FileSearchService}from'../common/file-search-service';import{QuickAccessContribution}from'@theia/core/lib/browser/quick-input/quick-access';exportdefaultnewContainerModule((bind:interfaces.Bind)=>{bind(FileSearchService).toDynamicValue(ctx=>{constprovider=ctx.container.get(WebSocketConnectionProvider);returnprovider.createProxy(fileSearchServicePath);}).inSingletonScope();bind(QuickFileOpenFrontendContribution).toSelf().inSingletonScope();[CommandContribution,KeybindingContribution,MenuContribution,QuickAccessContribution].forEach(serviceIdentifier=>bind(serviceIdentifier).toService(QuickFileOpenFrontendContribution));bind(QuickFileOpenService).toSelf().inSingletonScope();});注意:以下代码引入的是 Interface 而非具体实现。此外,Interface 同时也充当了标识符。import{KeybindingRegistry,KeybindingContribution,QuickAccessContribution}from'@theia/core/lib/browser';在这个例子中,InversifyJS 是链接代码模块的基础设施,即使处于同个 Package 中的不同模块, 也是通过 DI 访问的。如何给正在行驶的汽车换轮子,并且不让司机知道因为 IOC 的存在,只需要实现一个与原模块接口相同的模块,并且覆盖其绑定,就可以便捷地改变应用的行为。比如上文中的 QuickFileOpenService,如果对其行为不满意,可以创建一个 file-search-patched 的 Extension, 在其中实现一个新的 MyQuickFileOpenService,然后绑定到原抽象即可:import{QuickFileOpenService}from'@theia/file-search/lib/browser/quick-file-open';import{MyQuickFileOpenService}from'./my-quick-file-open';bind(QuickFileOpenService).to(MyQuickFileOpenService).inSingletonScope();神奇的是,QuickFileOpenFrontendContribution 仍然可以正常工作,尽管它:处于旧的 file-search 包中依赖了QuickFileOpenServiceQuickFileOpenFrontendContribution 通过 @inject 获取到我们提供的修改版 MyQuickFileOpenService,并且和旧实现接口兼容,所以 QuickFileOpenFrontendContribution 不需要做任何事情。如何保证所有使用 QuickFileOpenService 的地方都能获取到的新版实现?inject 发生于应用逻辑的运行时,而所有的 bind 都在应用入口就提前完成了。只要 bind 的顺序是确定的,那么可供 inject 的内容就是完全确定的。在 InversifyJS 的推荐的标准实践中, bind 集中发生在 Inversify.config 中,顺序当然是确定的。在 Theia 中,因为用户通过新增 Package 的方式扩展功能,所以 bind 自然分散在各模块中。但是 Theia 在构建时引入 Extension 的顺序是确定的,然后在应用逻辑启动前按顺序先完成所有模块的 bind,这样也就保证了 inject 的结果是确定的。基于 Theia 进行开发的体验直观来说,Theia 的这套体系解决了:如何在一个复杂系统中可靠地修改藏于深处的行为因为单一职责和 IOC,这些实现相对扁平且便捷清晰,并不难找Interface Driven 和 TS 提供了很强的约束/辅助通过新 Package 去覆盖内置模块的行为而非直接改动内置模块1、内置模块和用户扩展的功能有明确边界,保障核心稳定2、内置模块就是天然的文档3、便于二次开发,用户无需为了修改核心行为而从源头 Fork 后修改如何扩展一个复杂的系统利用 IOC 实现的 Contribution 机制[7]对开发者来说,解决了在哪写和怎么写这两个核心问题后,出错的可能就不多了。笔者之前写过几个 Theia 的 Extension,有一些还涉及了深度的定制。在缺乏文档的情况下,依靠 TS 和参照 Theia 官方 Package,就实现了功能。虽然 Theia 在其它方面设计也很优秀,但是如果没有基于 IOC 的这一套方法论,很难想象一个新手开发者经过简单的学习后可以对这样一个庞然大物进行二次开发,并且保证架构合理和功能可靠。没有银弹: IOC 的问题JavaScript 构建的应用不总是 OOP 的。IOC 或者整个 SOLID 理念脱胎于 Java 等传统技术生态,在开发习惯上存在差异。高效使用 IOC 需要相当的学习成本。在 Theia 中,也看到了不遵循 IOC 的实现。如何决定哪些地方使用 IOC, 哪些地方又可以突破限制,是一个需要工程经验和直觉的难题。IOC 推崇的 interface driven 需要良好的预先设计。在迭代快,需求快速变化的互联网领域,这是一个很强的假设。总结涉及到设计模式的讨论,总会有很多似是而非的观点。如何将设计模式落地到项目,真正地改善工程设计,是一个复杂的开放性问题,希望这篇文章可以给各位带来一些启发。点击上方关注 · 我们下期再见参考资料[1] Context – React: https://zh-hans.reactjs.org/docs/context.html#before-you-use-context[2] Modules: CommonJS modules | Node.js v19.4.0 Documentation: https://nodejs.org/api/modules.html#modules_cycles[3] ES6 Modules and Circular Dependency: https://stackoverflow.com/questions/46589957/es6-modules-and-circular-dependency[4] 译文:服务定位器 Service Locator 是一种反模式的设计: https://juejin.cn/post/7195850600503083066[5] Eclipse Theia : https://theia-ide.org/[6] Theia 的内置模块: https://github.com/eclipse-theia/theia/tree/master/packages[7] Contribution 机制: https://theia-ide.org/docs/frontend_application_contribution/
回复

使用道具 举报

您需要登录后才可以回帖 登录 | 会员注册

本版积分规则

QQ|手机版|心飞设计-版权所有:微度网络信息技术服务中心 ( 鲁ICP备17032091号-12 )|网站地图

GMT+8, 2025-1-13 22:32 , Processed in 2.693377 second(s), 26 queries .

Powered by Discuz! X3.5

© 2001-2025 Discuz! Team.

快速回复 返回顶部 返回列表