上一篇文章中提到,VS Code 中有许多服务(Services),分别提供不同模块的 API 以便其他模块调用,在需要依赖该服务的类构造函数中以 Decorator 修饰参数的形式声明依赖,调用者不需要显式的 new 这个服务,在调用者被创建时,这些依赖的服务也会被自动创建并传递给该调用者,同时不同的服务也可以相互依赖。这可以极大地降低程序耦合性,同时提高可维护性。
这种解耦程序的方式被称为 **依赖注入(DI),**是 **控制反转(IOC)**的一种实现方式。即对象(或实体)的依赖(可以是其他对象)通过外部注入,避免对象内部自身实现依赖的实例化过程。依赖注入在软件工程中有大量的实践应用,常见的服务端框架如 Spring、Laravel 等都是基于依赖注入的方式来管理对象之间的依赖关系。需要注意的是控制反转是一种设计思想,而依赖注入是这种思想的具体实现。
依赖注入的基本原理是由外部的容器来保存对象之间的依赖关系,同时这些对象的实例化也由容器来实现,这个容器被称为**依赖注入容器。**实际应用中我们的系统可能存在大量的对象或者服务,对象之间的依赖关系非常复杂,比如 B 依赖 A,则 A 就要先于 B 被实例化,这就要求外部的容器能够分析出这些对象的依赖关系。许多依赖注入框架还支持多种注入方式,比如构造函数注入、属性注入、方法参数注入等。
虽然依赖注入常见于传统后端框架,但作为一种设计模式,其适用于大部分基于面向对象思想构建的系统中。Google 出品的前端框架 Angular、今年大热的基于 TypeScript 的 Node.js 框架 nest.js 都大量使用了依赖注入实现程序的解耦,也有跨端(前端+ Node.js )的控制反转框架 InversifyJS ,同样在 VS Code 中也实现了一个轻量级的依赖注入模式,本文主要从源码出发详细剖析 VS Code 依赖注入的实现原理(VS Code 中主要使用构造函数注入)。
首先需要定义一个类并在其构造函数中声明依赖的服务
class MyClass {
constructor(
@IAuthService private readonly authService: IAuthService,
@IStorageService private readonly storageService: IStorageService,
) {
}
}
构造函数中的 @IAuthService 和 @IStorageService 是两个 Decorator(装饰器),装饰器在 JavaScript 中还属于一项提案,在 TypeScript 中是一项实验性特性。它可以被附加到类声明、方法、访问符以及参数上,在这段代码中他们被附加到了 MyClass 的构造函数参数 authService 和 storageService 上,也就是参数装饰器。参数装饰器会在运行时被函数调用,并传入三个参数:
服务的 Decorator 和接口定义一般如下
// 创建装饰器
export const IAuthService = createDecorator<IAuthService>('AuthService');
// 接口
export interface IAuthService {
readonly id: string;
readonly nickName: string;
readonly firstName: string;
readonly lastName: string;
requestService: IRequestService;
}
服务接口需要有具体实现,同时也允许依赖其他服务
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;
//...
}
}
还需要一个服务集,用于保存一组服务,并用其来创建一个容器
// 服务集
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);
}
}