在閱讀 VS Code 代碼的過程中,我們會發(fā)現(xiàn)每一個模塊中都有大量裝飾器的使用,用來裝飾模塊以及其中依賴的模塊變量。這樣做的目的是什么呢?在這一篇中我們來詳細(xì)分析一下。
依賴注入介紹
如果有這樣一個模塊 A,它的實現(xiàn)依賴另一個模塊 B 的能力,那么應(yīng)該如何設(shè)計呢?很簡單,我們可以在 A 模塊的構(gòu)造函數(shù)中實例化模塊 B,這樣就可以在模塊 A 內(nèi)部使用模塊 B 的能力了。
classA{
constructor(){
this.b=newB();
}
}
classB{}
consta=newA();
但是這樣做有兩個問題,一是模塊 A 的實例化過程中,需要手動實例化模塊 B,而且如果模塊 B 的依賴關(guān)系發(fā)生變化,那么也需要修改模塊 A 的構(gòu)造函數(shù),導(dǎo)致代碼耦合。
二是在復(fù)雜項目中,我們在實例化模塊 A 時,難以判斷模塊 B 是否被其他模塊依賴而已經(jīng)實例化過了,從而可能將模塊 B 多次實例化。若模塊 B 較重或者需要為單例設(shè)計,這將帶來性能問題。
因此,更好的方式是,將所有模塊的實例化交給外層框架,由框架統(tǒng)一管理模塊的實例化過程,這樣就可以解決上述兩個問題。
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);
這種將依賴對象通過外部注入,避免在模塊內(nèi)部實例化依賴的方式,稱為依賴注入 (Dependencies Inject, 簡稱 DI)。這在軟件工程中是一種常見的設(shè)計模式,我們在 Java 的 Spring,JS 的 Angular,Node 的 NestJS 等框架中都可以看到這種設(shè)計模式的應(yīng)用。
當(dāng)然,在實際應(yīng)用中,由于模塊眾多,依賴復(fù)雜,我們很難像上面的例子一樣,規(guī)劃出來每個模塊的實例化時機,從而編寫模塊實例化順序。并且,許多模塊可能并不需要第一時間被創(chuàng)建,需要按需實例化,因此,粗暴的統(tǒng)一實例化是不可取的。
因此我們需要一個統(tǒng)一的框架來分析并管理所有模塊的實例化過程,這就是依賴注入框架的作用。
借助于 TypeScript 的裝飾器能力,VSCode 實現(xiàn)了一個極為輕量化的依賴注入框架。我們可以先來簡單實現(xiàn)一下,解開這個巧妙設(shè)計的神秘面紗。
最簡依賴注入框架設(shè)計
實現(xiàn)一個依賴注入框架只需要兩步,一個是將模塊聲明并注冊到框架中進行管理,另一個是在模塊構(gòu)造函數(shù)中,聲明所需要依賴的模塊有哪些。
我們先來看模塊的注冊過程,這需要 TypeScript 的類裝飾器能力。我們在注入時,只需要判斷模塊是否已經(jīng)注冊,如果沒有注冊,將模塊的 id(這里簡化為模塊 Class 名稱)與類型傳入即可完成單個模塊的注冊。
exportfunctionInjectable():ClassDecorator{
return(Target:Class):any=>{
if(!collection.providers.has(Target.name)){
collection.providers.set(Target.name,target);
}
returntarget;
};
}
之后我們再來看看模塊是如何聲明依賴的,這需要 TypeScript 的屬性裝飾器能力。我們在注入時,先判斷依賴的模塊是否已經(jīng)被實例化,如果沒有,則將依賴模塊進行實例化,并存入框架中管理。最終返回已經(jīng)被實例化完成的模塊實例。
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;
這樣,一個最簡化的依賴注入框架就完成了。由于保存了模塊的類型與實例,它實現(xiàn)了模塊的按需實例化,無需在項目啟動時就初始化所有模塊。
我們可以嘗試調(diào)用它,以上面舉出的例子為例:
@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 的依賴收集實現(xiàn)
上面介紹了一個依賴注入框架的最簡實現(xiàn)。但當(dāng)我們真正閱讀 VSCode 的源碼時,我們發(fā)現(xiàn) VSCode 中的依賴注入框架貌似并不是這樣消費的。
例如在下面這段鑒權(quán)服務(wù)中,我們發(fā)現(xiàn)該類并沒有@injectable()作為類的依賴收集,并且依賴服務(wù)也直接用其類名作為修飾器,而不是@inject()。
//srcvsworkbenchservicesauthenticationrowserauthenticationService.ts
exportclassAuthenticationServiceextendsDisposableimplementsIAuthenticationService{
constructor(
@IActivityServiceprivatereadonlyactivityService:IActivityService,
@IExtensionServiceprivatereadonlyextensionService:IExtensionService,
@IStorageServiceprivatereadonlystorageService:IStorageService,
@IRemoteAgentServiceprivatereadonlyremoteAgentService:IRemoteAgentService,
@IDialogServiceprivatereadonlydialogService:IDialogService,
@IQuickInputServiceprivatereadonlyquickInputService:IQuickInputService
){}
}
其實這里的修飾符并不是真正指向類名,而是一個同名的資源描述符 id(VSCode 中稱之為 ServiceIdentifier),通常使用字符串或 Symbol 標(biāo)識。
通過 ServiceIdentifier 作為 id,而不是簡單粗暴地通過類名稱作為 id 注冊 Service,有利于處理項目中一個 interface 可能存在多態(tài)實現(xiàn),需要同時多個同名類實例的問題。
此外,在構(gòu)造 ServiceIdentifier 時,我們便可以將該類聲明注入框架,而無需@injectable()顯示調(diào)用了。
那么,這樣一個 ServiceIdentifier 該如何構(gòu)造呢?
//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方法為類創(chuàng)建一個唯一的ServiceIdentifier,并將其作為修飾符即可。
以上面的 AuthenticationService 為例,若所依賴的 ActivityService 需要變更多態(tài)實現(xiàn),僅需修改 ServiceIdentifier 修飾符確定實現(xiàn)方式即可,無需更改業(yè)務(wù)的調(diào)用代碼。
exportconstIActivityServicePlanA=createDecorator("IActivityServicePlanA");
exportconstIActivityServicePlanB=createDecorator("IActivityServicePlanB");
exportinterfaceIActivityService{...}
exportclassAuthenticationService{
constructor(
@IActivityServicePlanAprivatereadonlyactivityService:IActivityService,
){}
}
循環(huán)依賴問題
模塊之間的依賴關(guān)系是有可能存在循環(huán)依賴的,比如 A 依賴 B,B 依賴 A。這種情況下進行兩個模塊的實例化會造成死循環(huán),因此我們需要在框架中加入循環(huán)依賴檢測機制來進行規(guī)避。
本質(zhì)上,一個健康的模塊依賴關(guān)系就是一個有向無環(huán)圖(DAG),我們之前介紹過有向無環(huán)圖在 excel 表格函數(shù)中的應(yīng)用,放在依賴注入框架的設(shè)計中也同樣適用。
我們可以通過深度優(yōu)先搜索(DFS)來檢測模塊之間的依賴關(guān)系,如果發(fā)現(xiàn)存在循環(huán)依賴,則拋出異常。
//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);
}
}
該方法通過獲取圖節(jié)點的出度,將該類的全部依賴提取出來作為roots,然后逐個實例化,并從途中剝離該依賴節(jié)點。由于依賴樹的構(gòu)建是逐層依賴的,因此按順序?qū)嵗纯?。?dāng)發(fā)現(xiàn)該類的所有依賴都被實例化后,圖中仍存在節(jié)點,則認(rèn)為存在循環(huán)依賴,拋出異常。
總結(jié)
本篇文章簡要介紹并實現(xiàn)了一個依賴注入框架,并解析了VSCode在實際問題上做出的一些改進。
實際上 VSCode 的依賴注入能力還有很多細(xì)節(jié)需要處理。例如異步實例化能力支持,通過封裝 Deferred 類取得Promise執(zhí)行狀態(tài),等等,在此就不一一展開了。感興趣的同學(xué)可以參考 VSCode 源碼:src/vs/platform/instantiation/common/instantiationService.ts,https://segmentfault.com/a/src/vs/platform/instantiation/common/instantiationService.ts做更進一步的學(xué)習(xí)。
審核編輯 :李倩
-
源碼
+關(guān)注
關(guān)注
8文章
651瀏覽量
29339 -
變量
+關(guān)注
關(guān)注
0文章
613瀏覽量
28434 -
vscode
+關(guān)注
關(guān)注
1文章
157瀏覽量
7766
原文標(biāo)題:VS Code 源碼深入淺出 -- 依賴注入設(shè)計
文章出處:【微信號:玩轉(zhuǎn)VS Code,微信公眾號:玩轉(zhuǎn)VS Code】歡迎添加關(guān)注!文章轉(zhuǎn)載請注明出處。
發(fā)布評論請先 登錄
相關(guān)推薦
評論