Maison  >  Article  >  outils de développement  >  Analyse détaillée de l'injection de dépendances dans VSCode

Analyse détaillée de l'injection de dépendances dans VSCode

青灯夜游
青灯夜游avant
2022-11-24 21:25:412362parcourir

Analyse détaillée de l'injection de dépendances dans VSCode

Dans le processus de lecture du code VSCode, nous constaterons qu'il existe un grand nombre de décorateurs utilisés dans chaque module pour décorer le module et les variables du module dont il dépend. Quel est le but de faire cela ? Dans cet article, nous l'analyserons en détail. [Apprentissage recommandé : Tutoriel vscode, Vidéo de programmation]

Introduction à l'injection de dépendances

S'il existe un tel module A et que sa mise en œuvre dépend des capacités d'un autre module B, comment doit-il être conçu ? Très simplement, nous pouvons instancier le module B dans le constructeur du module A, afin de pouvoir utiliser les capacités du module B à l'intérieur du module A.

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

class B {}

const a = new A();

Mais cela pose deux problèmes. Premièrement, pendant le processus d'instanciation du module A, le module B doit être instancié manuellement, et si les dépendances du module B changent, le constructeur du module A doit également être modifié, ce qui entraîne en couplage de code.

Deuxièmement, dans des projets complexes, lorsque nous instancions le module A, il est difficile de déterminer si le module B dépend d'autres modules et a déjà été instancié, le module B peut donc être instancié plusieurs fois. Si le module B est plus lourd ou doit être conçu comme un singleton, cela entraînera des problèmes de performances.

Par conséquent, une meilleure façon est de confier l'instanciation de tous les modules au framework externe et de laisser le framework gérer uniformément le processus d'instanciation des modules, afin que les deux problèmes ci-dessus puissent être résolus.

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

Cette méthode d'injection d'objets dépendants depuis l'extérieur pour éviter d'instancier des dépendances à l'intérieur du module est appelée Dependencies Inject (DI). Il s'agit d'un modèle de conception courant en génie logiciel. Nous pouvons voir l'application de ce modèle de conception dans des frameworks tels que Spring de Java, Angular de JS et NestJS de Node.

Bien sûr, dans les applications réelles, en raison du grand nombre de modules et de dépendances complexes, il nous est difficile de planifier le timing d'instanciation de chaque module et d'écrire la séquence d'instanciation du module comme dans l'exemple ci-dessus. De plus, de nombreux modules n'ont pas besoin d'être créés du premier coup et doivent être instanciés à la demande. Par conséquent, une instanciation unifiée grossière n'est pas recommandée. Nous avons donc besoin d'un framework unifié pour analyser et gérer le processus d'instanciation de tous les modules. C'est le rôle du framework d'injection de dépendances.

Avec l'aide des capacités de décorateur de TypeScript, VSCode implémente un framework d'injection de dépendances extrêmement léger. Nous pouvons d’abord le mettre en œuvre brièvement pour percer le mystère de cette conception ingénieuse.

La conception de framework d'injection de dépendances la plus simple

Il ne faut que deux étapes pour implémenter un framework d'injection de dépendances. L'une consiste à déclarer et à enregistrer le module dans le framework pour la gestion, et l'autre consiste à déclarer dans le constructeur de module les modules dont il a besoin. dépendre de .

Examinons d'abord le processus d'enregistrement du module, qui nécessite la capacité de décorateur de classe de TypeScript. Lors de l'injection, il nous suffit de déterminer si le module a été enregistré. Sinon, transmettre l'identifiant du module (ici simplifié en nom de classe du module) et le type peuvent compléter l'enregistrement d'un seul module.

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

Voyons ensuite comment les modules déclarent les dépendances, ce qui nécessite la capacité de décorateur de propriétés de TypeScript. Lors de l'injection, nous déterminons d'abord si le module dépendant a été instancié. Sinon, le module dépendant sera instancié et stocké dans le framework pour la gestion. Renvoie enfin l'instance de module qui a été instanciée.

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

En fin de compte, il vous suffit de vous assurer que le framework lui-même est instancié avant l'exécution du projet. (Désigné comme injecteur dans l'exemple)

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

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

De cette manière, un cadre d'injection de dépendances des plus simplifiés est complété. Étant donné que les types et les instances de modules sont enregistrés, cela permet l'instanciation des modules à la demande sans qu'il soit nécessaire d'initialiser tous les modules au démarrage du projet.

Nous pouvons essayer de l'appeler, en prenant l'exemple donné ci-dessus :

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

Il n'est pas nécessaire de connaître le timing d'instanciation des modules A et B, il suffit d'initialiser n'importe quel module directement, et le framework trouvera et instanciera automatiquement toutes les dépendances pour votre module.

Implémentation de la collection de dépendances de VSCode

Ce qui précède présente l'implémentation la plus simple d'un cadre d'injection de dépendances. Mais lorsque nous avons réellement lu le code source de VSCode, nous avons constaté que le framework d'injection de dépendances dans VSCode ne semble pas être utilisé de cette manière.

Par exemple, dans le service d'authentification suivant, nous avons constaté que la classe n'a pas @injectable() comme collection de dépendances de la classe, et que le service dépendant utilise également directement son nom de classe comme décorateur au lieu de @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
  ) {}
}

En fait, le modificateur ici ne pointe pas réellement vers le nom de la classe, mais vers un identifiant de descripteur de ressource portant le même nom (appelé ServiceIdentifier dans VSCode), qui est généralement identifié par une chaîne ou un symbole. . @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

Utiliser ServiceIdentifier comme identifiant au lieu de simplement enregistrer le service via le nom de classe comme identifiant est utile pour résoudre le problème selon lequel une interface du projet peut avoir une implémentation polymorphe et nécessiter plusieurs instances de la classe du même nom en même temps. 🎜

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

Ce qui précède est le contenu détaillé de. pour plus d'informations, suivez d'autres articles connexes sur le site Web de PHP en chinois!

Déclaration:
Cet article est reproduit dans:. en cas de violation, veuillez contacter admin@php.cn Supprimer