理解node中的依赖注入以及具体场景下的代码实现
依赖注入(Dependency Injection, DI)是一种设计模式,用于将对象或类所依赖的对象(即它们需要的服务或组件)从外部注入,而不是在内部创建。这种模式的主要目的是实现控制反转(Inversion of Control, IoC),提高代码的灵活性、可测试性和可维护性。
依赖注入的核心概念
-
依赖(Dependency):一个对象需要的另一个对象。例如,类
A
依赖于类B
,意味着A
需要B
的实例来完成其功能。 -
注入(Injection):将依赖项传递给依赖它们的对象。注入可以通过构造函数参数、方法参数或属性注入。
图例
这张图展示了一个依赖注入的示例,旨在通过订阅 MySQL 的 Binlog 日志来同步数据库,并进行离线计算。以下是对图中各部分的文字描述:
实例化消息队列
- EventHandlerInterface(事件处理接口)
- 定义了消息队列处理的标准接口。
- 具体实现类包括:
- KafkaEventHandler:根据 Kafka 标准实现。
- RocketMQEventHandler:根据 RocketMQ 标准实现。
- 这些实现类可以用于处理不同类型的消息队列。
告警消息推送系统
- AlertNotifierInterface(告警通知接口)
- 定义了告警消息推送的标准接口。
- 具体实现类包括:
- WebhookAlertNotifier:使用 Webhook 推送告警消息。
- DingdingAlertNotifier:使用钉钉机器人推送告警消息。
- FeishuAlertNotifier:使用飞书机器人推送告警消息。
- 这些实现类可以用于不同的告警通知渠道。
初始化 MySQL Binlog 事件处理器实例
- BinlogEventParser(Binlog 事件解析器)
- 接收两个依赖项:
- mq:实现了 EventHandlerInterface 的消息队列实例。
- webhook:实现了 AlertNotifierInterface 的告警消息推送实例。
- 通过依赖注入的方式,将消息队列实例和告警消息实例传入 BinlogEventParser。
- 负责解析 MySQL 的 Binlog 事件,并将事件同步到指定的消息队列和告警系统中。
- 接收两个依赖项:
依赖注入的优点
-
提高可测试性:通过注入依赖项,可以轻松地替换实际的依赖项为模拟对象(mock),从而进行单元测试。
-
降低耦合度:对象不再负责创建其依赖项,而是从外部获取它们,这降低了对象之间的耦合度。
-
提高可维护性:依赖关系更加明确,代码更容易理解和维护。
-
增强灵活性:可以在运行时动态地注入不同的实现,增强了系统的灵活性和扩展性。
依赖注入的实现
1. 手动注入
手动注入是最基本的依赖注入方式,通过构造函数、方法参数或属性传递依赖项。
构造函数注入
上面图例中就属于这种
class ServiceA {
constructor() {
this.name = 'ServiceA';
}
getName() {
return this.name;
}
}
class ServiceB {
constructor(serviceA) {
this.serviceA = serviceA;
}
printServiceAName() {
console.log(this.serviceA.getName());
}
}
const serviceA = new ServiceA();
const serviceB = new ServiceB(serviceA);
serviceB.printServiceAName(); // 输出: ServiceA
属性注入
class ServiceA {
constructor() {
this.name = 'ServiceA';
}
getName() {
return this.name;
}
}
class ServiceB {
setServiceA(serviceA) {
this.serviceA = serviceA;
}
printServiceAName() {
console.log(this.serviceA.getName());
}
}
const serviceA = new ServiceA();
const serviceB = new ServiceB();
serviceB.setServiceA(serviceA);
serviceB.printServiceAName(); // 输出: ServiceA
方法注入
class ServiceA {
constructor() {
this.name = 'ServiceA';
}
getName() {
return this.name;
}
}
class ServiceB {
printServiceAName(serviceA) {
console.log(serviceA.getName());
}
}
const serviceA = new ServiceA();
const serviceB = new ServiceB();
serviceB.printServiceAName(serviceA); // 输出: ServiceA
2. 工厂函数
使用工厂函数创建和传递依赖项,可以更灵活地管理依赖项的创建过程。
function createServiceA() {
return {
name: 'ServiceA',
getName() {
return this.name;
}
};
}
function createServiceB(serviceA) {
return {
printServiceAName() {
console.log(serviceA.getName());
}
};
}
const serviceA = createServiceA();
const serviceB = createServiceB(serviceA);
serviceB.printServiceAName(); // 输出: ServiceA
3. 依赖注入容器
使用依赖注入容器(如 InversifyJS)来管理和注入依赖项,可以更好地组织和管理复杂的依赖关系。
inversify
和reflect-metadata
是两个常用于 TypeScript 和 JavaScript 开发中的库,通常用于实现依赖注入和元数据反射。1.
inversify
:
- 依赖注入库:
inversify
是一个强大的依赖注入库,主要用于 TypeScript 和 JavaScript。依赖注入是一种设计模式,允许将对象的依赖关系从类内部移到外部,从而提高代码的模块化和可测试性。- 特性:
inversify
支持装饰器(decorators),可以轻松地将依赖注入到类中。它还支持容器(container),可以管理和解析依赖关系。2.
reflect-metadata
:
- 元数据反射库:
reflect-metadata
是一个用于添加和读取元数据的库。元数据是关于程序结构的信息,比如类的属性类型、方法参数类型等。reflect-metadata
库允许你在运行时访问这些信息。- 特性:
reflect-metadata
通常与 TypeScript 的装饰器一起使用,通过装饰器可以轻松地定义和访问元数据。
安装 InversifyJS
npm install inversify reflect-metadata
使用 InversifyJS
require('reflect-metadata');
const { Container, injectable, inject } = require('inversify');
// 定义接口和类
@injectable()
class ServiceA {
getName() {
return 'ServiceA';
}
}
@injectable()
class ServiceB {
constructor(@inject(ServiceA) serviceA) {
this.serviceA = serviceA;
}
printServiceAName() {
console.log(this.serviceA.getName());
}
}
// 创建容器并绑定类
const container = new Container();
container.bind(ServiceA).toSelf();
container.bind(ServiceB).toSelf();
// 获取实例并使用
const serviceB = container.get(ServiceB);
serviceB.printServiceAName(); // 输出: ServiceA
4. 使用框架
一些框架(如 NestJS)内置了依赖注入机制,简化了依赖注入的实现。
NestJS我用的不多,就不做过多介绍了。大家有兴趣去看看小册或者自己研究一下。我以往的文章中fastify+ws系列是用到了一些依赖注入机制的到时候我特别标注一下
安装 NestJS CLI
npm install -g @nestjs/cli
创建一个新的 NestJS 项目
nest new project-name
在 NestJS 中使用依赖注入
import { Injectable, Module } from '@nestjs/common';
@Injectable()
class ServiceA {
getName(): string {
return 'ServiceA';
}
}
@Injectable()
class ServiceB {
constructor(private readonly serviceA: ServiceA) {}
printServiceAName(): void {
console.log(this.serviceA.getName());
}
}
@Module({
providers: [ServiceA, ServiceB],
})
class AppModule {}
// 启动应用
import { NestFactory } from '@nestjs/core';
async function bootstrap() {
const app = await NestFactory.createApplicationContext(AppModule);
const serviceB = app.get(ServiceB);
serviceB.printServiceAName(); // 输出: ServiceA
}
bootstrap();
应用场景
场景 1: Web 应用中的控制器和服务
在一个典型的 Web 应用中,控制器负责处理用户请求,而服务则包含业务逻辑。通过依赖注入,可以轻松地将服务注入到控制器中,从而实现业务逻辑与控制器的解耦。
示例:Express.js 与依赖注入
const express = require('express');
const app = express();
class UserService {
getUser(id) {
return { id, name: 'John Doe' };
}
}
class UserController {
constructor(userService) {
this.userService = userService;
}
getUser(req, res) {
const user = this.userService.getUser(req.params.id);
res.json(user);
}
}
const userService = new UserService();
const userController = new UserController(userService);
app.get('/user/:id', (req, res) => userController.getUser(req, res));
app.listen(3000, () => {
console.log('Server running on port 3000');
});
场景 2: 微服务架构中的服务通信
在微服务架构中,不同的服务需要相互通信。通过依赖注入,可以轻松地管理和替换服务之间的通信逻辑。
示例:NestJS 中的微服务通信
import { Injectable, Module } from '@nestjs/common';
import { ClientsModule, Transport } from '@nestjs/microservices';
@Injectable()
class OrderService {
constructor(@Inject('PAYMENT_SERVICE') private readonly client: ClientProxy) {}
async createOrder(order) {
// 创建订单逻辑
await this.client.send({ cmd: 'process_payment' }, order).toPromise();
}
}
@Module({
imports: [
ClientsModule.register([
{ name: 'PAYMENT_SERVICE', transport: Transport.TCP },
]),
],
providers: [OrderService],
})
class OrderModule {}
场景 3: 日志记录和监控
在应用程序中,日志记录和监控是非常重要的功能。通过依赖注入,可以灵活地替换不同的日志记录和监控实现,满足不同环境的需求。
示例:Node.js 中的日志记录
class ConsoleLogger {
log(message) {
console.log(message);
}
}
class FileLogger {
log(message) {
// 将日志写入文件
}
}
class UserService {
constructor(logger) {
this.logger = logger;
}
createUser(user) {
this.logger.log('User created');
// 创建用户逻辑
}
}
const useFileLogger = process.env.NODE_ENV === 'production';
const logger = useFileLogger ? new FileLogger() : new ConsoleLogger();
const userService = new UserService(logger);
userService.createUser({ name: 'John Doe' });
场景 4: 数据库访问层
在大型应用中,可能会使用不同的数据库或数据存储技术。通过依赖注入,可以轻松地替换不同的数据库访问实现。
示例:使用 TypeORM 和 NestJS
import { Injectable, Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { User } from './user.entity';
@Injectable()
class UserRepository {
constructor(@InjectRepository(User) private readonly repo: Repository<User>) {}
async findUser(id: number): Promise<User> {
return this.repo.findOne(id);
}
}
@Module({
imports: [TypeOrmModule.forFeature([User])],
providers: [UserRepository],
})
class UserModule {}
场景 5: 配置管理
在应用程序中,配置管理是一个常见需求。通过依赖注入,可以轻松地注入不同的配置源,如环境变量、配置文件等。
示例:使用 dotenv 和依赖注入
require('dotenv').config();
class ConfigService {
constructor() {
this.config = {
dbHost: process.env.DB_HOST,
dbPort: process.env.DB_PORT,
};
}
get(key) {
return this.config[key];
}
}
class DatabaseService {
constructor(configService) {
this.configService = configService;
}
connect() {
const host = this.configService.get('dbHost');
const port = this.configService.get('dbPort');
console.log(`Connecting to database at ${host}:${port}`);
// 数据库连接逻辑
}
}
const configService = new ConfigService();
const databaseService = new DatabaseService(configService);
databaseService.connect(); // 输出: Connecting to database at localhost:5432
场景 6: 中间件和拦截器
在 Web 应用中,中间件和拦截器用于处理请求和响应。通过依赖注入,可以轻松地管理和替换不同的中间件和拦截器。
示例:Express.js 中的中间件
const express = require('express');
const app = express();
class AuthService {
isAuthenticated(req) {
// 认证逻辑
return true;
}
}
class AuthMiddleware {
constructor(authService) {
this.authService = authService;
}
handle(req, res, next) {
if (this.authService.isAuthenticated(req)) {
next();
} else {
res.status(401).send('Unauthorized');
}
}
}
const authService = new AuthService();
const authMiddleware = new AuthMiddleware(authService);
app.use((req, res, next) => authMiddleware.handle(req, res, next));
app.get('/secure', (req, res) => {
res.send('Secure endpoint');
});
app.listen(3000, () => {
console.log('Server running on port 3000');
});
总结
依赖注入是一种强大的设计模式,可以显著提高代码的质量和灵活性。根据项目的需求和复杂度,可以选择手动注入、使用工厂函数、依赖注入容器或框架来实现依赖注入。每种方法都有其优点和适用场景,选择合适的方法可以帮助你更好地管理项目中的依赖关系。
转载自:https://juejin.cn/post/7383884275228704777