写点什么

手写一个简单的 DI 类库

作者:易成管理学
  • 2024-06-19
    江西
  • 本文字数:8305 字

    阅读完需:约 27 分钟

简介

依赖注入(Dependency Injection,DI)是一种软件设计模式,旨在减少组件之间的耦合度,提高代码的可维护性、灵活性和可测试性。在依赖注入中,组件不再负责创建或管理其依赖项,而是从外部注入所需的依赖项。这种模式的核心思想是将依赖项从组件中解耦,让组件专注于自身的责任,而不必关注如何获取依赖项。

从一个例子开始


下面是一个简单的示例;有三个类: atabaseUserServiceAuthService ;依赖关系如下:

  • UserService 依赖于 Database

  • AuthService 依赖于 UserServiceDatabase


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();
复制代码


这一步可以做到的是,我们可以通过容器来注册服务的创建函数,并在需要时解析依赖关系。以下是使用示例:

// 注册 Databasecontainer.register(Database, () => {    return new Database();});
// 注册 UserServicecontainer.register(UserService, (container) => { const database = container.resolve<Database>(Database); return new UserService(database);});
// 注册 AuthServicecontainer.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.tstype 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.tsimport { 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 容器的实现还有很多细节和功能需要完善,如:循环依赖的处理、生命周期管理、作用域管理等。下面是示例中的完整代码。


用户头像

还未添加个人签名 2020-09-24 加入

分享技术知识、管理工具

评论

发布
暂无评论
手写一个简单的DI类库_软件设计_易成管理学_InfoQ写作社区