「这是我参与2022首次更文挑战的第15天,活动详情查看:2022首次更文挑战」。
前言
本文主要讲述如何使用TypeScript在Node.js环境中应用依赖注入。
介绍
每个软件程序都由基础模块构成而成。在面向对象编程范式中,我们使用类来构建复杂的架构。类似于使用积木搭建房子,我们将互相之间有联系的模块之间互相称之为“依赖”。这些被依赖的类为我们的提供了一系列复杂的操作或者行为。
一个类中可能含有其他的类作为其成员。那么,我们会面临以下几个问题:
- 这些依赖应该怎样被构建?是由我们组装这些对象,还是应该由其他程序来实例化他们?
- 倘若实例化这个类过于复杂,并且我们希望避免一些“面条式”的代码应该怎么办?
而**依赖注入(Dependency Injection)**就是为了上述问题。
在开始我们的例子之前,我们需要弄懂几个关于依赖注入的概念。依赖注入的思想告诉我们一个类应该将它的依赖作为参数,而不是直接实例化它们。你可以将复杂的部分从代码中抽取出来,并以另一种方式重新引入依赖项。但是如何抽取复杂逻辑和重新引入依赖项是依赖管理所面临的问题。你可以手动的操作所有的实例化和注入操作,但是这必然会导致一个十分复杂的系统,这是我们需要避免的情况。取而代之的是,你应该将创建对象的责任交给IoC容器(Inverse of Control 控制反转)
控制反转就是让一个容器能够控制程序依赖的所有依赖项。我们创建一个容器,该容器负责所有对象的创建工作。当一个类需要被实例化时,容器就会增加所需的依赖项。
IoC仅仅只是表达了一种方法,而不是一种具体的实现。为了应用依赖注入这种思想,我们需要一种DI框架。典型的DI框架有如下:
- Spring & Dagger —— Java
- Hilt —— Kotlin
- Unity —— C#
- Inversify, Nest.js, TypeDI —— TypeScript
总览 & 角色
在依赖注入的规则中,我们需要明白以下四种不同的角色的类型
- Client
- Service
- Interface
- Injector
Service是我们需要暴露的对象。该类被IOC容器管理并实例化。Client可以通过IOC容器来使用services。client不关心services中的具体细节,interface 能够确保client和service能够保持同步。当client需要依赖时,injector能够提供实例化的service。
依赖注入的方式:
我们能够以以下三种方式进行依赖注入:
-
我们能够通过 properties(field) 来应用依赖注入。在类中定义一个属性并在这个属性被调用时注入所需要的依赖。但是将属性暴露出来,违反了OOP的原则,因此,应该尽可能的避免这种类型的依赖注入。
-
我们还能够通过 methods 来应用依赖注入。一个对象的状态应该是私有的,当外部想要改变这个状态时,那么应该使用其
getter/setter方法。当你使用setter方法来实例化一个类的私有成员时,这就是方法注入 -
还可以通过 constructor 来应用依赖注入。该方法与构造一个对象的方式高度相似,且依赖关系清晰。所以我们通常推荐使用该种注入依赖的方式
使用TypeDI
只要我们明白了依赖注入的底层原理,那么不管使用哪个框架对于我们来说都没有太大的差异。本篇文章中,我选择了使用TypeScript编程语言和 TypeDI框架来实现一个简单的Demo来说明这些基础概念。
初始化项目可能需要花费一点时间,所以我这里提供一些简单的初始代码模板。你可以子这个GitHub仓库下载代码:mertturkmenoglu/typescript-dependency-injection: Medium TypeScript Dependency Injection Article Codes (github.com)
任意的TypeScript项目都可以用于演示DI,本文中使用了 Express App来进行演示。本文假设读者已经具有了基本的Express和 TypeScript的知识了。
该项目的入口文件为 src/index.ts ,该文件包含了启动项目的具体步骤:
import 'reflect-metadata';
import express from 'express';
import Container from 'typedi';
import UserController from './controllers/UserController';
const main = async () => {
const app = express();
const userController = Container.get(UserController);
app.get('/users', (req, res) => userController.getAllUsers(req, res));
app.listen(3000, () => {
console.log('Server started');
});
}
main().catch(err => {
console.error(err);
});
上述代码是一个非常简单的Express服务器,它仅仅拥有一个接口。当发起GET请求到 /user 路由时,会返回一个用户的列表。关键性的函数在 Container.get 方法。注意这里,我们并没有使用 new 关键字来实例化一个对象。我们仅仅只是要求IOC容器给我们一个 UserController 的实例。然后我们将相关的路由和控制器方法绑定在一起。
该应用虽然是一个用于演示的项目,但是我不希望它完全没有实际意义。我增加了不同的文件夹来表示后端的基础架构。它们分别是 控制器层(controller), 模型层(model), 数据层(repositories),服务层(services)。现在,让我们看看它们的作用。
- Controllers 目录包含了 REST风格的控制器,它们主要负责在用户和服务端之间进行沟通,它们接受用户的请求并返回结果。
- Models 目录包含了我们的数据库实体。虽然我们没有数据库链接,我们目前也不需要数据库,但是建立合适的项目结构有利于我们的学习。我们假设我们已经拥有了一个数据库实体。
- Services 目录包含了我们的服务。他们主要为Controller所服务来读取不同的repositories。
- Repositories 目录包含了读取数据库的类。我们使用数据映射技术来对数据库进行相关的操作。在该模式下,我们使用相关的类来读取数据库并进行一些操作。
我们不需要在一个类中做所有的事情。在请求和响应之间被划分成了许多层。这被称为 layered architechture。在不同的类中进行分工让我们更容易进行依赖注入。
import { Request, Response } from "express";
import { Service } from "typedi";
import UserService from "../services/UserService";
@Service()
class UserController {
constructor(private readonly userService: UserService) { }
async getAllUsers(_req: Request, res: Response) {
const result = await this.userService.getAllUsers();
return res.json(result);
}
}
export default UserController;
UserController 仅仅只有一个方法。 "getAllUsers" 方法从 user service 中获取结果并且将结果返回。我们为 UserController 类增加了一个 @Service 装饰器,这表示将这个类交由IOC容器进行管理。在构造函数中,我们可以看到该类需要一个 UserService 实例。再次强调,我们不需要控制依赖的实例化,因为 TypeDI 容器在生成UserController实例时会帮助我们创建 UserService 实例,也就是说会注入 UserService。
import { Service } from "typedi";
import User from "../models/User";
import UserRepository from "../repositories/UserRepository";
@Service()
class UserService {
constructor(private readonly userRepository: UserRepository) { }
async getAllUsers(): Promise<User[]> {
const result = await this.userRepository.getAllUsers();
return result;
}
}
export default UserService;
UserService 与 UserController 十分类似,我们为其增加一个 @Service 装饰器,并且我们在构造函数的参数列表中指定了需要依赖的类。
import { Service } from "typedi";
import User from "../models/User";
@Service()
class UserRepository {
private readonly users: User[] = [
{ name: 'Emily' },
{ name: 'John' },
{ name: 'Jane' },
];
async getAllUsers(): Promise<User[]> {
return this.users;
}
}
export default UserRepository;
UserRepository是我们的最后一个类。同样为类增加装饰器。但是这个类没有任何需要依赖的类。因为我们现在没有数据库链接,此处我仅仅只是硬编码了一个简单的用户列表。
总结
依赖注入为复杂的对象实例化提供了便利。虽然手动的进行依赖注入聊胜于无,但是使用 TypeDI 提供的IOC容器使这一切变得更加的容易和简单。
感谢您的时间阅读本文~
原文地址
Dependency Injection in TypeScript | by Mert Türkmenoğlu | Level Up Coding (gitconnected.com)