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容器被注入到对应的类中去。
相关推荐
转转技术团队44 分钟前
加Log就卡?不加Log就瞎?”——这个插件治好了我的精神
java·后端
谦行1 小时前
前端视角 Java Web 入门手册 5.5:真实世界 Web 开发——控制反转与 @Autowired
java·后端
uhakadotcom1 小时前
PyTorch 2.0:最全入门指南,轻松理解新特性和实用案例
后端·面试·github
bnnnnnnnn1 小时前
前端实现多服务器文件 自动同步宝塔定时任务 + 同步工具 + 企业微信告警(实战详解)
前端·javascript·后端
DataFunTalk1 小时前
乐信集团副总经理周道钰亲述 :乐信“黎曼”异动归因系统的演进之路
前端·后端·算法
DataFunTalk1 小时前
开源一个MCP+数据库新玩法,网友直呼Text 2 SQL“有救了!”
前端·后端·算法
idMiFeng2 小时前
通过GO后端项目实践理解DDD架构
后端
LemonDu2 小时前
Cursor入门教程-JetBrains过度向
人工智能·后端
LTPP2 小时前
掌握Rust Web开发的未来:Hyperlane框架全方位教程 🎓🔧
前端·后端·github
LemonDus2 小时前
Cursor入门教程-JetBrains过度向
后端·工具·技术知识