Heim  >  Artikel  >  Entwicklungswerkzeuge  >  Detaillierte Analyse der Abhängigkeitsinjektion in VSCode

Detaillierte Analyse der Abhängigkeitsinjektion in VSCode

青灯夜游
青灯夜游nach vorne
2022-11-24 21:25:412341Durchsuche

Detaillierte Analyse der Abhängigkeitsinjektion in VSCode

Beim Lesen des VSCode-Codes werden wir feststellen, dass in jedem Modul eine große Anzahl von Dekoratoren verwendet wird, um das Modul und die Modulvariablen, von denen es abhängt, zu dekorieren. Was ist der Zweck davon? In diesem Artikel werden wir es im Detail analysieren. [Empfohlenes Lernen: vscode-Tutorial, Programmiervideo]

Einführung in die Abhängigkeitsinjektion

Wenn es ein solches Modul A gibt und seine Implementierung von den Fähigkeiten eines anderen Moduls B abhängt, wie sollte es dann gestaltet sein? Ganz einfach: Wir können Modul B im Konstruktor von Modul A instanziieren, sodass wir die Fähigkeiten von Modul B innerhalb von Modul A nutzen können.

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

class B {}

const a = new A();

Dabei gibt es jedoch zwei Probleme: Erstens muss Modul B während des Instanziierungsprozesses manuell instanziiert werden, und wenn sich die Abhängigkeiten von Modul B ändern, muss auch der Konstruktor von Modul A geändert werden in der Codekopplung.

Zweitens ist es in komplexen Projekten beim Instanziieren von Modul A schwierig zu bestimmen, ob Modul B von anderen Modulen abhängig ist und bereits instanziiert wurde, sodass Modul B möglicherweise mehrmals instanziiert wird. Wenn Modul B schwerer ist oder als Singleton konzipiert werden muss, führt dies zu Leistungsproblemen.

Daher besteht eine bessere Möglichkeit darin, die Instanziierung aller Module an das äußere Framework zu übergeben und das Framework den Instanziierungsprozess der Module einheitlich verwalten zu lassen, sodass die beiden oben genannten Probleme gelöst werden können.

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

Diese Methode zum Einfügen abhängiger Objekte von außen, um die Instanziierung von Abhängigkeiten innerhalb des Moduls zu vermeiden, wird als Dependencies Inject (DI) bezeichnet. Dies ist ein gängiges Entwurfsmuster in der Softwareentwicklung. Wir können die Anwendung dieses Entwurfsmusters in Frameworks wie Javas Spring, JS's Angular und Node's NestJS sehen.

Natürlich ist es für uns in tatsächlichen Anwendungen aufgrund der großen Anzahl von Modulen und komplexen Abhängigkeiten schwierig, den Instanziierungszeitpunkt jedes Moduls zu planen und die Modulinstanziierungssequenz wie im obigen Beispiel zu schreiben. Darüber hinaus müssen viele Module möglicherweise nicht beim ersten Mal erstellt werden und müssen bei Bedarf instanziiert werden. Daher ist eine grobe einheitliche Instanziierung nicht ratsam. Wir benötigen also ein einheitliches Framework, um den Instanziierungsprozess aller Module zu analysieren und zu verwalten. Dies ist die Rolle des Dependency-Injection-Frameworks.

Mit Hilfe der Decorator-Funktionen von TypeScript implementiert VSCode ein extrem leichtes Dependency-Injection-Framework. Wir können es zunächst kurz umsetzen, um das Geheimnis dieses cleveren Designs zu lüften.

Das einfachste Dependency-Injection-Framework-Design

Es sind nur zwei Schritte erforderlich, um ein Dependency-Injection-Framework zu implementieren. Der erste besteht darin, das Modul für die Verwaltung im Framework zu deklarieren und zu registrieren, und der andere darin, im Modulkonstruktor zu deklarieren, welche Module es benötigt darauf angewiesen sein.

Sehen wir uns zunächst den Modulregistrierungsprozess an, der die Klassendekoratorfunktion von TypeScript erfordert. Bei der Injektion müssen wir nur feststellen, ob das Modul registriert wurde. Wenn nicht, kann die Registrierung eines einzelnen Moduls durch Übergabe der Modul-ID (hier vereinfacht als Modulklassenname) und des Typs abgeschlossen werden.

export function Injectable(): ClassDecorator {
  return (Target: Class): any => {
    if (!collection.providers.has(Target.name)) {
      collection.providers.set(Target.name, target);
    }
    return target;
  };
}

Als Nächstes werfen wir einen Blick darauf, wie Module Abhängigkeiten deklarieren, was die Eigenschaftsdekoratorfunktion von TypeScript erfordert. Bei der Injektion stellen wir zunächst fest, ob das abhängige Modul instanziiert wurde. Andernfalls wird das abhängige Modul instanziiert und zur Verwaltung im Framework gespeichert. Gibt schließlich die Modulinstanz zurück, die instanziiert wurde.

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

Am Ende müssen Sie nur sicherstellen, dass das Framework selbst instanziiert wird, bevor das Projekt ausgeführt wird. (Im Beispiel als Injektor bezeichnet)

export class ServiceCollection {
  readonly providers = new Map<string, any>();
  readonly dependencies = new Map<string, any>();
}

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

Auf diese Weise wird ein äußerst vereinfachtes Abhängigkeitsinjektions-Framework fertiggestellt. Da die Modultypen und Instanzen gespeichert werden, ist eine On-Demand-Instanziierung von Modulen möglich, ohne dass beim Start des Projekts alle Module initialisiert werden müssen.

Wir können versuchen, es aufzurufen, indem wir das oben gegebene Beispiel nehmen:

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

Es ist nicht erforderlich, den Instanziierungszeitpunkt von Modul A und B zu kennen. Initialisieren Sie einfach jedes Modul direkt, und das Framework findet und instanziiert automatisch alle Abhängigkeiten dafür Sie Modul.

VSCodes Implementierung der Abhängigkeitssammlung

Das Obige stellt die einfachste Implementierung eines Abhängigkeitsinjektions-Frameworks vor. Als wir jedoch den Quellcode von VSCode tatsächlich lasen, stellten wir fest, dass das Dependency-Injection-Framework in VSCode anscheinend nicht auf diese Weise genutzt wird.

Zum Beispiel haben wir im folgenden Authentifizierungsdienst festgestellt, dass die Klasse nicht über @injectable() als Abhängigkeitssammlung der Klasse verfügt und der abhängige Dienst seinen Klassennamen auch direkt als verwendet Dekorator anstelle von @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
  ) {}
}

Tatsächlich verweist der Modifikator hier nicht wirklich auf den Klassennamen, sondern auf eine Ressourcendeskriptor-ID mit demselben Namen (in VSCode ServiceIdentifier genannt), die normalerweise durch eine Zeichenfolge oder ein Symbol identifiziert wird . @injectable()作为类的依赖收集,并且依赖服务也直接用其类名作为修饰器,而不是@inject()

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

其实这里的修饰符并不是真正指向类名,而是一个同名的资源描述符 id(VSCode 中称之为 ServiceIdentifier),通常使用字符串或 Symbol 标识。

通过 ServiceIdentifier

Die Verwendung von ServiceIdentifier als ID, anstatt den Dienst einfach über den Klassennamen als ID zu registrieren, ist hilfreich, um das Problem zu lösen, dass eine Schnittstelle im Projekt möglicherweise eine polymorphe Implementierung hat und mehrere Instanzen davon erfordert die Klasse mit demselben Namen gleichzeitig aufrufen. 🎜

此外,在构造 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基础教程

Das obige ist der detaillierte Inhalt vonDetaillierte Analyse der Abhängigkeitsinjektion in VSCode. Für weitere Informationen folgen Sie bitte anderen verwandten Artikeln auf der PHP chinesischen Website!

Stellungnahme:
Dieser Artikel ist reproduziert unter:juejin.cn. Bei Verstößen wenden Sie sich bitte an admin@php.cn löschen