ホームページ  >  記事  >  開発ツール  >  VSCode での依存関係注入の詳細な分析

VSCode での依存関係注入の詳細な分析

青灯夜游
青灯夜游転載
2022-11-24 21:25:412342ブラウズ

VSCode での依存関係注入の詳細な分析

VSCode コードを読み取る過程で、モジュールとそのモジュールを装飾するために各モジュールで多数のデコレータが使用されていることがわかります。変数に依存します。これを行う目的は何ですか?この記事ではそれを詳しく分析します。 [推奨学習: vscode チュートリアル プログラミング ビデオ ]

依存性注入の概要

このようなモジュール A がある場合、その実装は以下に依存します。別のモジュール B にはその機能がありますが、どのように設計すればよいでしょうか?非常に簡単に言うと、モジュール A のコンストラクター内でモジュール B をインスタンス化することで、モジュール A 内でモジュール B の機能を使用できるようになります。

class A {
  constructor() {
    this.b = new B();
  }
}

class B {}

const a = new A();

しかし、これには 2 つの問題があります。まず、モジュール A のインスタンス化プロセス中に、モジュール B を手動でインスタンス化する必要があり、モジュール B の依存関係が変更された場合、モジュール A の構造もインスタンス化する必要があります。コードの結合につながる関数。

2 番目に、複雑なプロジェクトでは、モジュール A をインスタンス化するときに、モジュール B が他のモジュールに依存していて、すでにインスタンス化されているかどうかを判断するのが難しいため、モジュール B が複数回インスタンス化される可能性があります。モジュール B が重い場合、またはシングルトンとして設計する必要がある場合、パフォーマンスの問題が発生します。

したがって、すべてのモジュールのインスタンス化を外部フレームワークに引き渡し、フレームワークがモジュールのインスタンス化処理を一元管理することで、上記 2 つの問題を解決するのがよい方法です。

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);

モジュール内で依存関係がインスタンス化されるのを避けるために、外部から依存オブジェクトを挿入するこの方法は、依存関係インジェクト (DI) と呼ばれます。これはソフトウェア エンジニアリングにおける一般的な設計パターンであり、Java の Spring、JS の Angular、Node の NestJS などのフレームワークでこの設計パターンの適用を見ることができます。

もちろん、実際のアプリケーションでは、モジュールの数が多く、依存関係が複雑なため、各モジュールのインスタンス化のタイミングを計画し、上記の例のようにモジュールのインスタンス化シーケンスを記述することは困難です。さらに、多くのモジュールは最初に作成する必要がなく、オンデマンドで インスタンス化する必要があるため、大まかな統合インスタンス化はお勧めできません。

したがって、すべてのモジュールのインスタンス化プロセスを分析および管理するための統合フレームワークが必要であり、これが依存関係注入フレームワークの役割です。

TypeScript のデコレータ機能を利用して、VSCode は非常に軽量な依存関係注入フレームワークを実装します。まず簡単に実装して、この巧妙な設計の謎を解明します。

最も単純な依存関係注入フレームワークの設計

依存関係注入フレームワークを実装するには 2 つの手順だけが必要です。1 つは管理用にモジュールを宣言してフレームワークに登録することで、もう 1 つはモジュール コンストラクター。依存する必要があるモジュールを宣言します。

まず、TypeScript のクラス デコレータ機能が必要なモジュール登録プロセスを見てみましょう。インジェクトするときは、モジュールが登録されているかどうかを判断するだけでよく、登録されていない場合は、モジュールの ID (ここではモジュールのクラス名に簡略化しています) とタイプを渡すだけで、単一モジュールの登録を完了できます。

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);
  };
}

最後に必要なのは、プロジェクトを実行する前にフレームワーク自体がインスタンス化されていることを確認することだけです。 (例ではインジェクターとして表しています)

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() がクラスに存在せず、依存サービスもそのクラスを直接使用していることがわかりました。 name を修飾子として使用し、Not @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 と呼ばれます) を指しています。これは通常、文字列またはシンボル。

単純にクラス名を ID として使用するのではなく、ServiceIdentifier を ID として使用してサービスを登録します。これは、プロジェクト内のインターフェイスのポリモーフィックな実装の可能性を処理するのに役立ちます。同じ名前のクラスの複数のインスタンスが同時に必要になるという問題があります。

此外,在构造 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 中国語 Web サイトの他の関連記事を参照してください。

声明:
この記事はjuejin.cnで複製されています。侵害がある場合は、admin@php.cn までご連絡ください。