VSCode-IoC和DI

简要介绍

  • 控制反转(Inversion of Control) 是一种是面向对象编程中的一种设计原则,用来减低计算机代码之间的耦合度。其基本思想是:借助于"第三方"实现具有依赖关系的对象之间的解耦。

  • 由于引进了中间位置的"第三方",也就是IOC容器,使得A、B、C、D这4个对象没有了耦合关系,齿轮之间的传动全部依靠"第三方"了,全部对象的控制权全部上缴给"第三方"IOC容器,所以,IOC容器成了整个系统的关键核心。
  • 依赖注入(Dependency Injection) 就是将实例变量传入到一个对象中。控制反转 是一种思想,依赖注入是一种设计模式。IoC框架使用依赖注入作为实现控制反转的方式,但是控制反转还有其他的实现方式。

VSCode依赖注入简要介绍

  • VSCode 的代码是围绕着各式各样的 service 组织起来的,这些 service 基本都定义在 platform 那一层,通过构造函数注入器(constructor injection)注入到 client 中。
  • 一个 service 需要两部分定义:1. service 接口定义 2. service 标志符。service 标志符是一个装饰器(Decorator是ES7的提案)并且必须和 service 接口同名。
  • 在VSCode的编码实现中,我们会发现实现一个服务所需要的通用步骤:
    • 定义服务接口:首先需要定义服务接口,该接口定义了服务的 API,即服务能够提供哪些功能。接口通常放在 vs/platform 文件夹下的一个子文件夹中,比如 vs/platform/telemetry/common/telemetry。
    • 定义与服务器同名的Identifier,比如export const IProductService = createDecorator('productService');。
    • 注册服务:其次需要在应用程序的入口处,即 vs/code/electron-main/main.ts 中注册服务,并将其添加到容器中(以Identifier为key,实例或者SyncDescriptor实例作为value)。
  • VS Code 中依赖注入的实现主要在 vs/platform/instantiation/common 文件夹下,如下:
    • descriptors.ts 服务实例包装类
    • extensions.ts 通用服务注册、获取服务
    • graph.ts 基于有向图的依赖分析
    • instantiation.ts 服务实例(创建 Decorator、存储服务的依赖)
    • instantiationService.ts 容器
    • serviceCollection.ts 服务集合
  • VSCode的依赖注入大致实现的原理就是:
    • 第一次会将需要注入的服务形成一个有向图
    • 第二次会遍历这个有向图然后生成对应的实例注入。使用图的数据结构可以清晰地看到依赖关系链路,并且能够快速识别出循环依赖问题。

基本用法

  • 首先需要定义一个类并在其构造函数中声明依赖的服务
less 复制代码
class MyClass {
	constructor(
		@IAuthService private readonly authService: IAuthService,
		@IStorageService private readonly storageService: IStorageService,
	) {
	}
}
  • 构造函数中的 @IAuthService 和 @IStorageService 是两个 Decorator(装饰器),装饰器在 JavaScript 中还属于一项提案,在 TypeScript 中是一项实验性特性。它可以被附加到类声明、方法、访问符以及参数上,在这段代码中他们被附加到了 MyClass 的构造函数参数 authService 和 storageService 上,也就是参数装饰器。参数装饰器会在运行时被函数调用,并传入三个参数:
  1. 对于静态成员来说是类的构造函数,对于实例成员是类的原型对象
  2. 成员的名字
  3. 参数在函数参数列表中的索引
  • 服务的 Decorator 和接口定义一般如下
typescript 复制代码
// 创建装饰器
export const IAuthService = createDecorator<IAuthService>('AuthService');
// 接口
export interface IAuthService {
	readonly id: string;
	readonly nickName: string;
	readonly firstName: string;
	readonly lastName: string;
  requestService: IRequestService;
}
  • 服务接口需要有具体实现,同时也允许依赖其他服务
typescript 复制代码
class AuthServiceImpl implements IAuthService {
  constructor(
		@IRequestService public readonly requestService IRequestService,		
  ){
  }

  public async getUserInfo() {
		const { id, nickName, firstName } = await getUserInfo();
		this.id = id;
		this.nickName = nickName;
		this.firstName = firstName;
		//...
	} 
}
  • 还需要一个服务集,用于保存一组服务,并用其来创建一个容器
typescript 复制代码
// 服务集
export class ServiceCollection {

	private _entries = new Map<ServiceIdentifier<any>, any>();

	constructor(...entries: [ServiceIdentifier<any>, any][]) {
		for (let [id, service] of entries) {
			this.set(id, service);
		}
	}

	set<T>(id: ServiceIdentifier<T>, instanceOrDescriptor: T | SyncDescriptor<T>): T | SyncDescriptor<T> {
		const result = this._entries.get(id);
		this._entries.set(id, instanceOrDescriptor);
		return result;
	}

	forEach(callback: (id: ServiceIdentifier<any>, instanceOrDescriptor: any) => any): void {
		this._entries.forEach((value, key) => callback(key, value));
	}

	has(id: ServiceIdentifier<any>): boolean {
		return this._entries.has(id);
	}

	get<T>(id: ServiceIdentifier<T>): T | SyncDescriptor<T> {
		return this._entries.get(id);
	}
}
  • 前文说到对象由容器自动实例化,实际上在 VSCode 中一些服务没有其他依赖(例如日志服务),仅被其他服务所依赖,所以可以手动实例化并注册到容器中。而这个例子中 AuthServiceImpl 还依赖 IRequestService,需要用 SyncDescriptor 封装一下保存在服务集中
csharp 复制代码
const services = new ServiceCollection(); // 创建一个服务集
const logService = new LogService(); // 直接实例化一个服务

services.set(ILogService, logService);
services.set(IAuthService, new SyncDescriptor(AuthServiceImpl)); // 第一个参数即服务的装饰器
  • SyncDescriptor 是一个用于包装需要被容器实例化容器的描述符对象,它保存了对象的构造器和静态参数(需要被直接传递给构造函数)
typescript 复制代码
export class SyncDescriptor<T> {

	readonly ctor: any;
	readonly staticArguments: any[];
	readonly supportsDelayedInstantiation: boolean;

	constructor(ctor: new (...args: any[]) => T, staticArguments: any[] = [], supportsDelayedInstantiation: boolean = false) {
		this.ctor = ctor; // 服务的构造器
		this.staticArguments = staticArguments; // 静态参数
		this.supportsDelayedInstantiation = supportsDelayedInstantiation; // 是否支持延迟实例化
	}
}
  • 到这里我们可以创建容器并把服务注册到容器中了,VSCode 中容器是 InstantiationService
ini 复制代码
const instantiationService = new InstantiationService(services, true);
  • InstantiationService 是依赖注入的核心,当服务被注册到容器后,我们需要先手动实例化程序入口,在 VSCode 中即是 CodeApplication,容器(instantiationService)保存着这些对象的依赖关系,所以 CodeApplication 也需要借助容器来实例化。
scss 复制代码
// 这里第二和第三个参数是 CodeApplication 构造器的静态参数,需要手动传递进去
instantiationService.createInstance(CodeApplication, mainIpcServer, instanceEnvironment).startup();
  • 同时也可以手动获取服务实例,需要调用 instantiationService.invokeFunction 方法,传入一个回调函数,其参数是一个访问器,当通过访问器获取指定服务时,容器会自动去分析它所依赖的服务并自动实例化后返回。
ini 复制代码
instantiationService.invokeFunction(accessor => {
	const logService = accessor.get(ILogService);
	const authService = accessor.get(IAuthService);
});
  • instantiationService 包含一个成员方法 createChild ,可以创建一个子容器,为了更好地划分依赖关系,子容器可以访问父容器中的服务实例,反之父容器则无法访问子容器的实例,当子容器中不存在所需要的服务实例时会调用 instantiationService._parent 获取父容器的引用并逐层往上查找依赖。
  • 以上是 VSCode 中实现依赖注入的基本用法,相比传统 Spring 等框架来说简单了不少,没有那么多种注入方式,不需要将依赖关系写到单独某个文件中,同时也提供了手动获取依赖及实例化的机制。
  • 总体来说,VSCode除了可以运用这种方式来实现Service的自动注入以外,还可以通过这种方式来自由地、灵活地加载在不同环境下的Service服务。

实现原理

  • 一开始调用 createDecorator 函数定义了一个服务的装饰器,用于在构造函数中声明依赖关系以方便注入依赖。createDecorator 的主要作用是返回一个装饰器
typescript 复制代码
export function createDecorator<T>(serviceId: string): { (...args: any[]): void; type: 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('@IServiceName-decorator can only be used to decorate a parameter');
		}
    // 将服务作为依赖保存在为目标类的属性中
		storeServiceDependency(id, target, index, false);
	};

	id.toString = () => serviceId;

	_util.serviceIds.set(serviceId, id);
	return id;
}
  • 同时调用 storeServiceDependency 函数将传入的服务 ID (唯一的字符串)及索引保存在所装饰类的一个成员 <math xmlns="http://www.w3.org/1998/Math/MathML"> d i di </math>didependencies 数组中
ini 复制代码
function storeServiceDependency(id: Function, target: Function, index: number, optional: boolean): void {
	if (target[_util.DI_TARGET] === target) {
		target[_util.DI_DEPENDENCIES].push({ id, index, optional });
	} else {
		target[_util.DI_DEPENDENCIES] = [{ id, index, optional }];
		target[_util.DI_TARGET] = target;
	}
}
  • 其中 _util.DI_DEPENDENCIES 和 _util.DI_TARGET 分别是两个 magic string
dart 复制代码
export const DI_TARGET = '$di$target';
export const DI_DEPENDENCIES = '$di$dependencies';
  • 对于第一个例子中 MyClass,其构造函数中的两个装饰器在编译时会被自动执行,并将依赖的服务记录到 <math xmlns="http://www.w3.org/1998/Math/MathML"> d i di </math>didependencies
dart 复制代码
MyClass['$di$dependencies'] = [
	{ id: 'AuthService', index: 0, optional: false },
  { id: 'StorageService', index: 1, optional: false }
];

MyClass['$di$target'] = MyClass;
  • 对于被装饰器装饰过的入参,会直接通过IoC容器被注入到对应的类中去。
相关推荐
coderSong25682 小时前
Java高级 |【实验八】springboot 使用Websocket
java·spring boot·后端·websocket
Mr_Air_Boy3 小时前
SpringBoot使用dynamic配置多数据源时使用@Transactional事务在非primary的数据源上遇到的问题
java·spring boot·后端
打码人的日常分享3 小时前
物联网智慧医院建设方案(PPT)
大数据·物联网·架构·流程图·智慧城市·制造
咖啡啡不加糖4 小时前
Redis大key产生、排查与优化实践
java·数据库·redis·后端·缓存
白水baishui4 小时前
搭建强化推荐的决策服务架构
架构·推荐系统·强化学习·决策服务·服务架构
何双新4 小时前
第23讲、Odoo18 邮件系统整体架构
ai·架构
雪碧聊技术4 小时前
将单体架构项目拆分成微服务时的两种工程结构
微服务·架构·module·project·工程结构
大鸡腿同学4 小时前
纳瓦尔宝典
后端
从零开始学习人工智能5 小时前
Doris 数据库深度解析:架构、原理与实战应用
数据库·架构