首頁 >開發工具 >VSCode >詳細分析一下VSCode中的依賴注入

詳細分析一下VSCode中的依賴注入

青灯夜游
青灯夜游轉載
2022-11-24 21:25:412428瀏覽

詳細分析一下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, any>();
  readonly dependencies = new Map<string, any>();
}

const collection = new ServiceCollection();
export default collection;

這樣,一個最簡化的依賴注入框架就完成了。由於保存了模組的類型與實例,它實現了模組的按需實例化,無需在專案啟動時就初始化所有模組。

我們可以嘗試呼叫它,以上面舉出的例子為例:

@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()

// src\vs\workbench\services\authentication\browser\authenticationService.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 该如何构造呢?

// src\vs\platform\instantiation\common\instantiation.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(&#39;@IServiceName-decorator can only be used to decorate a parameter&#39;);
    }
    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;
  }
}

我们仅需通过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,
  ) {}
}

循环依赖问题

模块之间的依赖关系是有可能存在循环依赖的,比如 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 源码:src/vs/platform/instantiation/common/instantiationService.ts,做更进一步的学习。

附录

最简 DI 框架完整 demo:github.com/realDuang/d…

更多关于VSCode的相关知识,请访问:vscode基础教程

以上是詳細分析一下VSCode中的依賴注入的詳細內容。更多資訊請關注PHP中文網其他相關文章!

陳述:
本文轉載於:juejin.cn。如有侵權,請聯絡admin@php.cn刪除