简介
依赖注入(Dependency Injection,DI)是一种软件设计模式,旨在减少组件之间的耦合度,提高代码的可维护性、灵活性和可测试性。在依赖注入中,组件不再负责创建或管理其依赖项,而是从外部注入所需的依赖项。这种模式的核心思想是将依赖项从组件中解耦,让组件专注于自身的责任,而不必关注如何获取依赖项。
1.从一个例子开始
下面是一个简单的示例;有三个类: Database
、 UserService
和 AuthService
;依赖关系如下:UserService
依赖于 Database
AuthService
依赖于 UserService
和 Database
class Database { }
class UserService {
constructor(
private database: Database
) { }
}
class AuthService {
constructor(
private userService: UserService,
private database: Database
) { }
}
在 TypeScript 中,可以按以下步骤实例化 AuthService :
// 创建 Database 的实例
const database = new Database();
// 创建 UserService 的实例,并将 Database 的实例传递给构造函数
const userService = new UserService(database);
// 创建 AuthService 的实例,并将 UserService 和 Database 的实例传递给构造函数
const authService = new AuthService(userService, database);
在这个示例中,我们扮演的角色其实就是个 DI,我们手动创建了 Database
、 UserService
和 AuthService
的实例,并将它们的依赖关系手动传递给构造函数。这种方式存在以下问题:
- 代码重复:在创建实例时,我们需要手动传递依赖关系,这样会导致代码重复。
- 依赖关系耦合:组件之间的依赖关系是硬编码的,这样会导致组件之间的耦合度增加。
- 可测试性差:由于依赖关系是硬编码的,我们无法轻松地替换依赖项,这会导致测试困难。
2.使用简单的容器管理依赖项
假设我们使用一个简单的容器来管理依赖项的创建和注入。在这个示例中,我们将使用一个简单的对象作为容器,并通过该容器来注册服务的创建函数,并在需要时解析依赖关系。这样做的好处是可以更方便地管理依赖项的创建和注入,避免了手动管理依赖项的复杂性。以下是容器的定义和使用示例:
// src/di/container.ts
class Container {
// 用于缓存已经创建的实例
private cache = new Map<Token, any>();
// 存储服务提供者的创建函数
private providers = new Map<Token, { creator: (container: Container) => any }>();
// 解析依赖关系并返回实例
public resolve<T>(token: Token): T {
// 如果实例已经存在于缓存中,则直接返回
if (this.cache.has(token)) return this.cache.get(token) as T;
// 查找对应的服务提供者
const provider = this.providers.get(token);
if (provider === undefined) throw new Error(`No provider registered for token ${token}`);
// 调用服务提供者的创建函数创建实例
const instance = provider.creator(this);
// 将创建的实例缓存起来
this.cache.set(token, instance);
return instance;
}
// 注册服务提供者
public register<T>(token: Token, providerCreator: (container: Container) => T) {
this.providers.set(token, { creator: providerCreator });
}
}
// 导出容器的单例实例
export const container = new Container();
这一步可以做到的是,我们可以通过容器来注册服务的创建函数,并在需要时解析依赖关系。以下是使用示例:
// 注册 Database
container.register(Database, () => {
return new Database();
});
// 注册 UserService
container.register(UserService, (container) => {
const database = container.resolve<Database>(Database);
return new UserService(database);
});
// 注册 AuthService
container.register(AuthService, (container) => {
const userService = container.resolve<UserService>(UserService);
const database = container.resolve<Database>(Database);
return new AuthService(userService, database);
});
// 解析依赖关系并获取 AuthService 的实例
const authService = container.resolve<AuthService>(AuthService);
3. 提供类的注册方法
上面的示例中,我们手动注册了每个类的创建函数,这样会导致代码重复。我们可以将注册类的创建函数抽象出来,这样可以更加方便地注册类的依赖关系。以下是 registerClass
函数的实现:
class Container {
//...
// 注册服务
public register<T>(token: Token, providerCreator: (container: Container) => T) {
this.providers.set(token, {
creator: providerCreator,
});
}
// 注册类的创建函数
public registerClass<T>(token: Token, _class: Type<T>, deps:Token[]=[]) {
this.register(token, (container) => {
const args = deps.map(dep => container.resolve(dep));
return new _class(...args);
});
}
}
使用示例:
container.registerClass(Database, Database);
container.registerClass(UserService, UserService, [Database]);
container.registerClass(AuthService, AuthService, [UserService,Database]);
4.支持更多的注册方法
除了可以注册其他类外,依赖注入还可以用于注入各种类型的依赖,如值、工厂函数等。这种灵活性使得依赖注入成为一个强大的设计模式,用于管理组件之间的各种依赖关系。下面针对不同的注入类型实现特定的注册方法
// 使用枚举定义提供者的类型
enum ProviderType {
useClass,
useValue
}
class Container {
private cache = new Map<Token, any>();
private providers = new Map<Token, { type?: ProviderType, creator: (container: Container) => any }>;
public resolve<T>(token: Token): T {
if (this.cache.has(token)) return this.cache.get(token) as T;
const provider = this.providers.get(token);
if (provider === undefined) throw new Error(`...`);
const instance = provider.creator(this);
if (provider.type != ProviderType.useFactory) {
this.cache.set(token, instance);
}
return instance;
}
public register<T>(token: Token, type: ProviderType, providerCreator: (container: Container) => T) {
this.providers.set(token, { type, creator: providerCreator });
}
public registerClass<T>(token: Token,_class_: Type<T>,deps:Token[]=[]) {
this.register(token, (container) => {
const args = deps.map(dep => container.resolve(dep));
return new _class_(...args);
});
}
// ++注册值++
public registerValue<T>(token: Token, value: T) {
this.register(token, ProviderType.useValue,()=>{
return value
});
}
// ++册工厂函数++
public registerFactory<T>(token: Token, factory: (...args) => T, depsTokens: Token[] = []) {
const args: any[] = depsTokens.map(token => this.resolve(token));
this.register(token, ProviderType.useFactory, () => factory(...args));
}
// ++注册现有实例++
public registerExisting<T>(token: Token, existingToken: Token) {
this.providers.set(token, {
type: ProviderType.useExisting,
creator: () => this.resolve(existingToken)
});
}
}
使用示例:
// 注册 token 为 "number" 的值为 "123" 到容器中
container.registerValue("number", 123);
// 工厂模式注入
container.registerFactory("userService1", (database) => {
return new UserService(database);
}, [Database]);
// 注册 token 为 "number" 的值为 "123" 到容器中
container.registerExisting("UserService2", Database);
上面通过例子实现了四种注册方式,分别为 registerClass、registerValue、registerFactory、registerExisting,这四种方式分别对应了四种不同的依赖注入场景。除此之外,还有一些其他的场景没有列出,都可以通过类似的方式来实现。
5.支持配置化注册服务
通过配置化的方式注册服务,可以更加灵活地管理服务的依赖关系。在这个示例中,我们将提供一个配置对象,用于指定服务的 token 和提供者类型,以及其他相关的配置信息。这样可以更加灵活地管理服务的依赖关系,避免了手动注册服务的复杂性。
enum ProviderType {
useClass,
useValue,
useFactory,
useExisting,
}
class Container {
// ...
public registerFromProviders<T>(providers: {
token: Token,
[key in ProviderType]: any,
deps?: { token: Token }[]
}[]) {
providers.forEach(provider => {
if (provider.useClass) {
this.registerClass(provider.token, provider.useClass, deps);
} else if (provider.useValue) {
this.registerValue(provider.token, provider.useValue);
} else if (provider.useFactory) {
this.registerFactory(provider.token, provider.useFactory,deps);
} else if (provider.useExisting) {
this.registerExisting(provider.token, provider.useExisting);
} else if (isFunction(provider)){
this.registerClass(provider, provider);
}else{
throw new Error(`Invalid provider type for token ${provider.token}`);
}
})
}
}
使用示例:
container.registerFromProviders([
UserService,
{
token: Database,
useClass: Database
},
{
token: "applicationType",
useValue: 130
},
{
token: "databaseAlias",
useExisting: Database
}
]);
6.组件依赖信息收集
上面的示例中我们是在 registerClass
时候手动的的传入类的依赖,这个类的依赖还是人工收集出来的,我们提供 metadata 收集和缓存相关的逻辑
// metadata.ts
type ComponentMetadata= {
type: Type;
arguments: Token[];
}
const componentMetadataMap = new Map<Type, ComponentMetadata>([]);
const injectable = () => (target: Type) => {
const arguments = Reflect.getMetadata('design:paramtypes', target) || [];
const metadata = componentMetadataMap.get(target) || { type: target, arguments:arguments };
componentMetadataMap.set(target, metadata);
}
同时需要修改 Container 的 registerClass 方法 ,让其支持从 ComponentMeta 中获取依赖
import { componentMetadataMap } from 'metadata.ts';
class Container {
// ...
public registerClass<T>(token: Token,_class_: Type<T>,deps?:Token[]) {
// 依赖支持从ComponentMeta中获取
if(!deps){
deps = componentMetadataMap.get(_class)?.arguments||[];
}
this.register(token, (container) => {
const args = deps.map(dep => container.resolve(dep));
return new _class_(...args);
});
}
简化后的使用
@injectable()
class Database { }
@injectable()
class UserService {
constructor(
private userService: UserService
) { }
}
@injectable()
class AuthService {
constructor(
private userService: UserService,
private database: Database
) { }
}
container.registerFromProviders([
Database,
UserService,
AuthService
])
const authService=container.resolve<AuthService>(AuthService)
7.通过指定token注入依赖
通过TS装饰器来获取的依赖并不完全可靠;比如注入属性的类型为 interface 时,TS编译时不能拿到正确的类型,所以对于非Type类型的注入,需要指定对应的Token,如下面的例子
@injectable()
class AuthService {
constructor(
private userService: UserService,
@inject("applicationType") private applicationType: number
) { }
}
container.registerFromProviders([
{
token: "applicationType",
useValue: 130
}
Database,
UserService,
AuthService
])
修改 metadata.ts
type ComponentMetadata<T = unknown> = {
type: Type<T>;
arguments: Token[];
}
const componentMetadataMap = new Map<Type, ComponentMetadata>([]);
export const injectable = (options: { provideIn?: "root" } = {}) => (target: Type) => {
const _arguments = Reflect.getMetadata('design:paramtypes', target) || [];
const metadata = componentMetadataMap.get(target) || { type: target, arguments: [] };
_arguments.forEach((arg: Token, index: number) => {
metadata.arguments[index] = metadata.arguments[index] || {
token: arg
};
})
if (options) {
metadata.provideIn = options.provideIn;
}
componentMetadataMap.set(target, metadata);
}
export const inject = (token: Token, optional?: boolean) => (target: any, key: string, index: number) => {
if (key === "constructor") {
const metadata = componentMetadataMap.get(target) || { type: target, arguments: [] };
metadata.arguments[index] = {
token,
optional
};
componentMetadataMap.set(target, metadata);
}
}
8.使用模块组织代码
type ComponentMetadata<T = unknown> = {
type: Type<T>;
arguments: Token[];
providers?:Provider[]
}
const componentMetadataMap = new Map<Type, ComponentMetadata>([]);
export const DIModule = (moduleOptions: {
providers: Provider[],
}) => (target) => {
injectable()(target)
const metadata = componentMetadataMap.get(target)
metadata.providers = moduleOptions.providers;
}
修改 Container,提供从模块创建容器的方法
import { componentMetadataMap } from 'metadata.ts';
class Container {
//...
static fromModule(module:Type){
const metadata = componentMetadataMap.get(module);
const container =new Container();
container.registerFromProviders(metadata.providers);
return container;
}
}
接下来我们可以通过模块的方式来组织代码
@DIModule({
prividers: [
Database,
{ token: UserService, useClass: UserService },
{ token: AuthService, useClass: AuthService }
]
})
class AppModule { }
const container=Container.fromModule(AppModule)
const authService=container.resolve<AuthService>(AuthService)
9.支持子容器
到目前为止,我们只实现了一个全局的容器,所有的服务都注册在这个容器中。但是在实际应用中,我们可能需要多个容器,每个容器负责不同的服务。为了支持多个容器,我们可以实现一个子容器的概念,子容器可以继承父容器的服务,并且可以注册自己的服务。以下是一个可能的实现方式:
/ metadata.ts
type ComponentMetadata<T = unknown> = {
type: Type<T>;
arguments: Token[];
providers?:Provider[];
imports?:Provider[]
}
const componentMetadataMap = new Map<Type, ComponentMetadata>([]);
export const DIModule = (moduleOptions: {
providers: Provider[],
}) => (target) => {
injectable()(target)
const metadata = componentMetadataMap.get(target)
metadata.providers = moduleOptions.providers;
metadata.imports = moduleOptions.imports;
}
// container.ts
import { componentMetadataMap } from 'metadata.ts';
class Container {
constructor(private parent?: Container) { }
private subModules = new Map<Type, Container>();
public resolve<T>(token: Token): T {
// 先在当前容器中查找依赖,如果没有找到的话去 this.parent.resolve 去查找依赖
}
//...
static fromModule(module: Type, parent?: Container) {
const metadata = componentMetadataMap.get(module);
const container = new Container(parent);
container.registerFromProviders(metadata.providers);
// 如果模块有导入的话,创建子容器
if (metadata.imports?.length) {
metadata.imports.forEach((subModule: Type) => {
const subContainer = Container.fromModule(subModule, container);
container.registerValue(subModule, subModule);
container.subContainers.set(subModule, subContainer);
})
}
return container;
}
public getSubContainer<T>(token: Token): Container {
return this.subModules.get(token);
}
}
下面是一个使用子容器的示例:
// idea.module.ts
@DIModule({
providers: [
Database,
{ token: UserService, useClass: UserService },
{ token: AuthService, useClass: AuthService }
]
})
class IdeaModule { }
// app.module.ts
@DIModule({
providers: [
{ token: "ApplicationType", useClass: "ship" }
],
imports: [IdeaModule]
})
class AppModule { }
const container = Container.fromModule(AppModule);
const authService = container.getSubContainer(IdeaModule)
.resolve<AuthService>(AuthService);
10.内置模块/容器
为了更好的管理容器,我们可以实现一些内置的模块,如:NullContainer,RootContainer、AppContainer等。这些内置模块可以提供一些默认的行为,如:注册所有的provideIn为root的服务、抛出错误等。
|---NullContainer
|---RootContainer
|---AppContainer
- NullContainer : 内置容器,子模块为 AppContainer , 复写 resolve 方法,抛出错误
- RootContainer : 内置容器,初始化时会注册 componentMetadata 中所有 provideIn:"root" 的组件
- AppContainer: 应用的根模块,通过启动函数(bootstrap/run) 来启动模块初始化;
class NullContainer extends Container {
resolve(token: Token) {
throw new Error(`...`);
}
}
export const nullContainer = new NullContainer();
export const rootContainer = new Container(nullContainer);
// 提供 bootstrapApplication 函数,用于初始化容器
export function bootstrapApplication(appModule: Type){
componentMetadataMap.forEach((metadata, token) => {
if (metadata.provideIn === 'root') {
rootContainer.registerClass(token, metadata.type);
}
})
return Container.fromModule(appModule, rootContainer);
}
使用
@DIModule({
providers: [
Database,
{ token: UserService, useClass: UserService },
{ token: AuthService, useClass: AuthService }
]
})
class AppModule { }
const container = bootstrapApplication(AppModule);
总结
我们通过实现一个简易的DI容器来更加深刻的了解依赖注入的原理和实现方式。作为一个依赖框架,DI容器的实现还有很多细节和功能需要完善,如:循环依赖的处理、生命周期管理、作用域管理等。下面是示例中的完整代码。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。