简介

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

1.从一个例子开始

下面是一个简单的示例;有三个类: DatabaseUserServiceAuthService ;依赖关系如下:
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,我们手动创建了 DatabaseUserServiceAuthService 的实例,并将它们的依赖关系手动传递给构造函数。这种方式存在以下问题:

  • 代码重复:在创建实例时,我们需要手动传递依赖关系,这样会导致代码重复。
  • 依赖关系耦合:组件之间的依赖关系是硬编码的,这样会导致组件之间的耦合度增加。
  • 可测试性差:由于依赖关系是硬编码的,我们无法轻松地替换依赖项,这会导致测试困难。

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容器的实现还有很多细节和功能需要完善,如:循环依赖的处理、生命周期管理、作用域管理等。下面是示例中的完整代码。


PingCode研发中心
111 声望22 粉丝