问小白 wenxiaobai
资讯
历史
科技
环境与自然
成长
游戏
财经
文学与艺术
美食
健康
家居
文化
情感
汽车
三农
军事
旅行
运动
教育
生活
星座命理

如何在Node.js里实现依赖注入

创作时间:
作者:
@小白创作中心

如何在Node.js里实现依赖注入

引用
CSDN
1.
https://m.blog.csdn.net/zmh_fuhuasishui/article/details/142862489

依赖注入是软件工程中一个重要的设计模式,它可以帮助开发者实现控制反转(IoC),降低组件间的耦合度,提高代码的可维护性和可测试性。本文将详细介绍如何在Node.js中实现依赖注入,包括手动注入、自动注入以及使用第三方库的具体方法。

什么是依赖注入

依赖注入是一种用于在开发过程中实现控制反转(IoC)的技术。在IoC中,对程序流的控制是颠倒的:依赖项不是控制其依赖项的创建和管理的组件,而是从外部源提供给组件。

在传统的编程模式中,一个组件可能会直接创建并管理它所依赖的其他组件,这会导致组件之间的耦合度较高,难以维护和测试。

控制反转是一种设计原则,它改变了组件之间的控制关系。在IoC中,组件不再自己创建和管理它所依赖的组件,而是将这种控制权交给外部。具体来说,依赖注入是IoC的一种实现方式,它通过外部源(比如容器或框架)来提供组件所需的依赖项。

这样做的好处是:

  • 解耦:组件不再直接依赖于具体的依赖项实现,而是依赖于抽象的接口或抽象类,这样可以降低组件之间的耦合度。
  • 易于维护:由于组件之间的依赖关系是由外部控制的,因此修改一个组件的依赖项时,不需要修改组件本身的代码,只需要调整外部的配置或代码。
  • 易于测试:在单元测试时,可以轻松地替换组件的依赖项为模拟对象(mock objects),从而可以独立地测试组件的功能。
  • 可重用性:由于组件不直接依赖于具体的实现,而是依赖于抽象,这使得组件更容易在不同的上下文中被重用。

如何实现

了解完定义,我们来看一下案例。先看一个没有使用依赖注入的例子:

手动注入

// Dependency.js
class Dependency {
  constructor() {
    this.name = 'Dependency';
  }
}

// Service.js
class Service {
  constructor(dependency) {
    this.dependency = dependency;
  }
  greet() {
    console.log(`Hello, I depend on ${this.dependency.name}`);
  }
}

// App.js
const Dependency = require('./Dependency');
const Service = require('./Service');
const dependency = new Dependency();
const service = new Service(dependency);
service.greet();

这里展示了一个简单的依赖注入模式。Service 依赖于 dependency 对象,在创建了 Service 类的实例时,将 dependency 实例作为参数传递给 Service 的构造函数,这样 Service 就依赖于 Dependency

自动注入

手动注入毕竟太麻烦,而且依赖的实例多的时候,每个都通过形参传入不太靠谱,下面我们来看看如何实现自动注入。

// Dependency.js
export class Dependency {
  constructor() {
    this.name = 'Dependency';
  }
}

// Service.js
export class Service {
  constructor(dependency) {
    this.dependency = dependency;
  }
  greet() {
    console.log(`Hello, I depend on ${this.dependency.name}`);
  }
}

// Container.js
import { Dependency } from './Dependency';
import { Service } from './Service';

export class Container {
  constructor() {
    this.dependencyInstances = new Map();
    this.dependencyConstructors = new Map([
      [Dependency, Dependency],
      [Service, Service],
    ]);
  }

  getDependency(ctor) {
    if (!this.dependencyInstances.has(ctor)) {
      const dependencyConstructor = this.dependencyConstructors.get(ctor);
      if (!dependencyConstructor) {
        throw new Error(`No dependency registered for ${ctor.name}`);
      }
      const instance = new dependencyConstructor(this.getDependency.bind(this));
      this.dependencyInstances.set(ctor, instance);
    }
    return this.dependencyInstances.get(ctor);
  }
}

// App.js
import { Container } from './Container';
import { Service } from './Service';
import { Dependency } from './Dependency';

const container = new Container();
const service = container.getDependency(Service);
service.greet();

这里增加了 Container 用于管理实例,我们只需要维护对应的依赖关系,在需要使用的时候再创建对应的实例。是不是很简单?简单才是王道,使用过egg的小伙伴都知道egg里只需要导出Class,我们就可以直接在context里访问对应的实例。

// app/controller/user.js
const Controller = require('egg').Controller;

class UserController extends Controller {
  async info() {
    const { ctx } = this;
    const userId = ctx.params.id;
    const userInfo = await ctx.service.user.find(userId);
    ctx.body = userInfo;
  }
}

module.exports = UserController;

// app/service/user.js
const Service = require('egg').Service;

class UserService extends Service {
  async find(uid) {
    // 假如我们拿到用户 id,从数据库获取用户详细信息
    const user = await this.ctx.db.query(
      'select * from user where uid = ?',
      uid
    );
    // 假定这里还有一些复杂的计算,然后返回需要的信息
    const picture = await this.getPicture(uid);
    return {
      name: user.user_name,
      age: user.age,
      picture,
    };
  }
}

module.exports = UserService;

egg里的实现其实更彻底,直接使用了getter替代了 container.getDependency(Service),使用了本地文件读取加载class实例。其实现如下:

// define ctx.service
Object.defineProperty(app.context, property, {
  get() {
    // eslint-disable-next-line @typescript-eslint/no-this-alias
    const ctx = this;
    // distinguish property cache,
    // cache's lifecycle is the same with this context instance
    // e.x. ctx.service1 and ctx.service2 have different cache
    if (!ctx[CLASS_LOADER]) {
      ctx[CLASS_LOADER] = new Map();
    }
    const classLoader: Map<string | symbol, ClassLoader> = ctx[CLASS_LOADER];
    let instance = classLoader.get(property);
    if (!instance) {
      instance = getInstance(target, ctx);
      classLoader.set(property, instance!);
    }
    return instance;
  },
});

function getInstance(values: any, ctx: ContextDelegation) {
  // it's a directory when it has no exports
  // then use ClassLoader
  const Class = values[EXPORTS] ? values : null;
  let instance;
  if (Class) {
    if (isClass(Class)) {
      instance = new Class(ctx);
    } else {
      // it's just an object
      instance = Class;
    }
  // Can't set property to primitive, so check again
  // e.x. module.exports = 1;
  } else if (isPrimitive(values)) {
    instance = values;
  } else {
    instance = new ClassLoader({ ctx, properties: values });
  }
  return instance;
}

优先从缓存里读取实例,不存在则执行getInstance,其实现如下:

function getInstance(values: any, ctx: ContextDelegation) {
  // it's a directory when it has no exports
  // then use ClassLoader
  const Class = values[EXPORTS] ? values : null;
  let instance;
  if (Class) {
    if (isClass(Class)) {
      instance = new Class(ctx);
    } else {
      // it's just an object
      instance = Class;
    }
  // Can't set property to primitive, so check again
  // e.x. module.exports = 1;
  } else if (isPrimitive(values)) {
    instance = values;
  } else {
    instance = new ClassLoader({ ctx, properties: values });
  }
  return instance;
}

优先从缓存里加载,如果缓存不存在则主动去加载一次。

第三方库

除了自己实现之外,我们也可以借助第三方的库,如 InversifyJS、Awilix 等。这些库提供了更高级的功能,如依赖的自动解析、生命周期管理等。下面是使用 InversifyJS 的一个基本示例:

首先,安装 InversifyJS:

npm install inversify reflect-metadata --save

然后,我们可以这样使用它:

const { injectable, inject, Container } = require('inversify');
require('reflect-metadata');

// 定义依赖
@injectable()
class Logger {
  log(message) {
    console.log(message);
  }
}

@injectable()
class EmailService {
  constructor(@inject(Logger) logger) {
    this.logger = logger;
  }
  sendEmail(to, content) {
    // 发送邮件的逻辑...
    this.logger.log(`Sending email to ${to}`);
  }
}

// 设置容器
const container = new Container();
container.bind(Logger).toSelf();
container.bind(EmailService).toSelf();

// 从容器中获取实例
const emailService = container.get(EmailService);

// 使用服务
emailService.sendEmail('example@example.com', 'Hello, Dependency Injection with InversifyJS!');

在这个例子中,我们使用了 InversifyJS 的装饰器来标记 Logger 和 EmailService 是可注入的。我们还创建了一个 Container 来管理我们的依赖,然后从容器中获取了 EmailService 的实例。

总结

依赖注入是一个强大的模式,它可以帮助我们构建更加灵活、可维护和可测试的Node.js应用程序。无论是手动实现还是使用专门的库,依赖注入都值得在我们的工具箱中占有一席之地。通过将依赖注入作为应用程序架构的一部分,我们可以提高代码质量,并为未来的扩展打下坚实的基础。

© 2023 北京元石科技有限公司 ◎ 京公网安备 11010802042949号