likes
comments
collection
share

理解node中的依赖注入以及具体场景下的代码实现

作者站长头像
站长
· 阅读数 30

依赖注入(Dependency Injection, DI)是一种设计模式,用于将对象或类所依赖的对象(即它们需要的服务或组件)从外部注入,而不是在内部创建。这种模式的主要目的是实现控制反转(Inversion of Control, IoC),提高代码的灵活性、可测试性和可维护性。

依赖注入的核心概念

  1. 依赖(Dependency):一个对象需要的另一个对象。例如,类 A 依赖于类 B,意味着 A 需要 B 的实例来完成其功能。

  2. 注入(Injection):将依赖项传递给依赖它们的对象。注入可以通过构造函数参数、方法参数或属性注入。

图例

理解node中的依赖注入以及具体场景下的代码实现

这张图展示了一个依赖注入的示例,旨在通过订阅 MySQL 的 Binlog 日志来同步数据库,并进行离线计算。以下是对图中各部分的文字描述:

实例化消息队列

  • EventHandlerInterface(事件处理接口)
    • 定义了消息队列处理的标准接口。
    • 具体实现类包括:
      • KafkaEventHandler:根据 Kafka 标准实现。
      • RocketMQEventHandler:根据 RocketMQ 标准实现。
    • 这些实现类可以用于处理不同类型的消息队列。

告警消息推送系统

  • AlertNotifierInterface(告警通知接口)
    • 定义了告警消息推送的标准接口。
    • 具体实现类包括:
      • WebhookAlertNotifier:使用 Webhook 推送告警消息。
      • DingdingAlertNotifier:使用钉钉机器人推送告警消息。
      • FeishuAlertNotifier:使用飞书机器人推送告警消息。
    • 这些实现类可以用于不同的告警通知渠道。

初始化 MySQL Binlog 事件处理器实例

  • BinlogEventParser(Binlog 事件解析器)
    • 接收两个依赖项:
      • mq:实现了 EventHandlerInterface 的消息队列实例。
      • webhook:实现了 AlertNotifierInterface 的告警消息推送实例。
    • 通过依赖注入的方式,将消息队列实例和告警消息实例传入 BinlogEventParser。
    • 负责解析 MySQL 的 Binlog 事件,并将事件同步到指定的消息队列和告警系统中。

依赖注入的优点

  1. 提高可测试性:通过注入依赖项,可以轻松地替换实际的依赖项为模拟对象(mock),从而进行单元测试。

  2. 降低耦合度:对象不再负责创建其依赖项,而是从外部获取它们,这降低了对象之间的耦合度。

  3. 提高可维护性:依赖关系更加明确,代码更容易理解和维护。

  4. 增强灵活性:可以在运行时动态地注入不同的实现,增强了系统的灵活性和扩展性。

依赖注入的实现

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
评论
请登录