在閱讀 VSCode 代碼的過程中,我們會發現每一個模塊中都有大量裝飾器的使用,用來裝飾模塊以及其中依賴的模塊變量。這樣做的目的是什么呢?在這一篇中我們來詳細分析一下。【推薦學習:VSCode、VSCode】
依賴注入介紹
如果有這樣一個模塊 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<string>(); ??readonly?dependencies?=?new?Map<string>(); } const?collection?=?new?ServiceCollection(); export?default?collection;</string></string>
這樣,一個最簡化的依賴注入框架就完成了。由于保存了模塊的類型與實例,它實現了模塊的按需實例化,無需在項目啟動時就初始化所有模塊。
我們可以嘗試調用它,以上面舉出的例子為例:
@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()。
//?srcvsworkbenchservicesauthenticationbrowserauthenticationService.ts export?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<t>(serviceId:?string):?ServiceIdentifier<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); ??}; ??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; ??} }</any></t></t>
我們僅需通過createDecorator方法為類創建一個唯一的ServiceIdentifier,并將其作為修飾符即可。
以上面的 AuthenticationService 為例,若所依賴的 ActivityService 需要變更多態實現,僅需修改 ServiceIdentifier 修飾符確定實現方式即可,無需更改業務的調用代碼。
export?const?IActivityServicePlanA?=?createDecorator<iactivityservice>("IActivityServicePlanA"); export?const?IActivityServicePlanB?=?createDecorator<iactivityservice>("IActivityServicePlanB"); export?interface?IActivityService?{...} export?class?AuthenticationService?{ ??constructor( ????@IActivityServicePlanA?private?readonly?activityService:?IActivityService, ??)?{} }</iactivityservice></iactivityservice>
循環依賴問題
模塊之間的依賴關系是有可能存在循環依賴的,比如 A 依賴 B,B 依賴 A。這種情況下進行兩個模塊的實例化會造成死循環,因此我們需要在框架中加入循環依賴檢測機制來進行規避。
本質上,一個健康的模塊依賴關系就是一個有向無環圖(DAG),我們之前介紹過有向無環圖在 excel 表格函數中的應用,放在依賴注入框架的設計中也同樣適用。
我們可以通過深度優先搜索(DFS)來檢測模塊之間的依賴關系,如果發現存在循環依賴,則拋出異常。
//?src/vs/platform/instantiation/common/instantiationService.ts 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); ??} }
該方法通過獲取圖節點的出度,將該類的全部依賴提取出來作為roots,然后逐個實例化,并從途中剝離該依賴節點。由于依賴樹的構建是逐層依賴的,因此按順序實例化即可。當發現該類的所有依賴都被實例化后,圖中仍存在節點,則認為存在循環依賴,拋出異常。
總結
本篇文章簡要介紹并實現了一個依賴注入框架,并解析了VSCode在實際問題上做出的一些改進。
實際上 VSCode 的依賴注入能力還有很多細節需要處理。例如異步實例化能力支持,通過封裝 Deferred 類取得promise執行狀態,等等,在此就不一一展開了。感興趣的同學可以參考 VSCode 源碼:VSCode,做更進一步的學習。
附錄
最簡 DI 框架完整 demo:VSCode
更多關于VSCode的相關知識,請訪問:VSCode!