如何在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;
      },
    });

优先从缓存里读取实例,不存在则执行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;
}

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

第三方库

除了自己实现之外,我们也可以借助第三方的库,如InversifyJSAwilix等。这些库提供了更高级的功能,如依赖的自动解析、生命周期管理等。下面是使用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的装饰器来标记LoggerEmailService是可注入的。我们还创建了一个Container来管理我们的依赖,然后从容器中获取了EmailService的实例。

总结

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

本文由mdnice多平台发布

相关推荐
小镇程序员1 分钟前
vue2 src_Todolist全局总线事件版本
前端·javascript·vue.js
野槐3 分钟前
前端图像处理(一)
前端
程序猿阿伟11 分钟前
《智能指针频繁创建销毁:程序性能的“隐形杀手”》
java·开发语言·前端
疯狂的沙粒12 分钟前
对 TypeScript 中函数如何更好的理解及使用?与 JavaScript 函数有哪些区别?
前端·javascript·typescript
瑞雨溪21 分钟前
AJAX的基本使用
前端·javascript·ajax
力透键背24 分钟前
display: none和visibility: hidden的区别
开发语言·前端·javascript
程楠楠&M35 分钟前
node.js第三方Express 框架
前端·javascript·node.js·express
盛夏绽放43 分钟前
Node.js 和 Socket.IO 实现实时通信
前端·后端·websocket·node.js
想自律的露西西★1 小时前
用el-scrollbar实现滚动条,拖动滚动条可以滚动,但是通过鼠标滑轮却无效
前端·javascript·css·vue.js·elementui·前端框架·html5
白墨阳1 小时前
vue3:瀑布流
前端·javascript·vue.js