手写一个简单的 DI 类库
简介
依赖注入(Dependency Injection,DI)是一种软件设计模式,旨在减少组件之间的耦合度,提高代码的可维护性、灵活性和可测试性。在依赖注入中,组件不再负责创建或管理其依赖项,而是从外部注入所需的依赖项。这种模式的核心思想是将依赖项从组件中解耦,让组件专注于自身的责任,而不必关注如何获取依赖项。
从一个例子开始
下面是一个简单的示例;有三个类: atabase
、 UserService
和 AuthService
;依赖关系如下:
UserService
依赖于Database
AuthService
依赖于UserService
和Database
在 TypeScript 中,可以按以下步骤实例化 AuthService
:
在这个示例中,我们扮演的角色其实就是个 DI,我们手动创建了 Database
、 UserService
和 AuthService
的实例,并将它们的依赖关系手动传递给构造函数。这种方式存在以下问题:
代码重复:在创建实例时,我们需要手动传递依赖关系,这样会导致代码重复。
依赖关系耦合:组件之间的依赖关系是硬编码的,这样会导致组件之间的耦合度增加。
可测试性差:由于依赖关系是硬编码的,我们无法轻松地替换依赖项,这会导致测试困难。
2. 使用简单的容器管理依赖项
假设我们使用一个简单的容器来管理依赖项的创建和注入。在这个示例中,我们将使用一个简单的对象作为容器,并通过该容器来注册服务的创建函数,并在需要时解析依赖关系。这样做的好处是可以更方便地管理依赖项的创建和注入,避免了手动管理依赖项的复杂性。以下是容器的定义和使用示例:
这一步可以做到的是,我们可以通过容器来注册服务的创建函数,并在需要时解析依赖关系。以下是使用示例:
3. 提供类的注册方法
上面的示例中,我们手动注册了每个类的创建函数,这样会导致代码重复。我们可以将注册类的创建函数抽象出来,这样可以更加方便地注册类的依赖关系。以下是
registerClass
函数的实现:
使用示例:
4 支持更多的注册方法
除了可以注册其他类外,依赖注入还可以用于注入各种类型的依赖,如值、工厂函数等。这种灵活性使得依赖注入成为一个强大的设计模式,用于管理组件之间的各种依赖关系。下面针对不同的注入类型实现特定的注册方法
使用示例:
上面通过例子实现了四种注册方式,分别为 registerClass、registerValue、registerFactory、registerExisting,这四种方式分别对应了四种不同的依赖注入场景。除此之外,还有一些其他的场景没有列出,都可以通过类似的方式来实现。
5. 支持配置化注册服务
通过配置化的方式注册服务,可以更加灵活地管理服务的依赖关系。在这个示例中,我们将提供一个配置对象,用于指定服务的 token 和提供者类型,以及其他相关的配置信息。这样可以更加灵活地管理服务的依赖关系,避免了手动注册服务的复杂性。
使用示例:
6. 组件依赖信息收集
上面的示例中我们是在
registerClass
时候手动的的传入类的依赖,这个类的依赖还是人工收集出来的,我们提供 metadata 收集和缓存相关的逻辑
同时需要修改 Container 的 registerClass 方法 ,让其支持从 ComponentMeta 中获取依赖
简化后的使用
7. 通过指定 token 注入依赖
通过 TS 装饰器来获取的依赖并不完全可靠;比如注入属性的类型为 interface 时,TS 编译时不能拿到正确的类型,所以对于非 Type 类型的注入,需要指定对应的 Token,如下面的例子
修改 metadata.ts
8. 使用模块组织代码
修改 Container,提供从模块创建容器的方法
接下来我们可以通过模块的方式来组织代码
9. 支持子容器
到目前为止,我们只实现了一个全局的容器,所有的服务都注册在这个容器中。但是在实际应用中,我们可能需要多个容器,每个容器负责不同的服务。为了支持多个容器,我们可以实现一个子容器的概念,子容器可以继承父容器的服务,并且可以注册自己的服务。以下是一个可能的实现方式:
下面是一个使用子容器的示例:
10. 内置模块/容器
为了更好的管理容器,我们可以实现一些内置的模块,如:NullContainer,RootContainer、AppContainer 等。这些内置模块可以提供一些默认的行为,如:注册所有的 provideIn 为 root 的服务、抛出错误等。
NullContainer : 内置容器,子模块为 AppContainer , 复写 resolve 方法,抛出错误
RootContainer : 内置容器,初始化时会注册 componentMetadata 中所有 provideIn:"root" 的组件
AppContainer: 应用的根模块,通过启动函数(bootstrap/run) 来启动模块初始化;
使用
总结
我们通过实现一个简易的 DI 容器来更加深刻的了解依赖注入的原理和实现方式。作为一个依赖框架,DI 容器的实现还有很多细节和功能需要完善,如:循环依赖的处理、生命周期管理、作用域管理等。下面是示例中的完整代码。
评论