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

news2024/10/12 9:15:14
alt

什么是依赖注入

依赖注入是一种用于在开发过程中实现控制反转(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 多平台发布

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2207600.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【ECC校验及使用】

ECC校验及使用 1 RAM ECC Demo 在IC设计中会经常用到ram来进行存储&#xff0c;在存储数据时我们写读时需要知道在传输过程中有没有出现差错&#xff0c;因此需要额外的校验位也存储下来&#xff1b; ECC验证能纠正1bit错误&#xff0c;能发现2bit错误&#xff0c;但是无法纠正…

第十三章:监控过程组(13.1控制质量--13.7监督沟通)

前言&#xff1a; 监督 是收集项目绩效数据&#xff0c;计算绩效指标&#xff0c;并报告和发布绩效信息。 控制 是比较实际绩效与计划绩效&#xff0c;分析偏差&#xff0c;评估趋势以改进过程&#xff0c;评价可选方案&#xff0c;并建 议必要的纠正措施。 13.1 控制质量 本过…

数据库SQL基础教程(二)

目录 连接(JOIN) 语法&#xff1a; 不同的 SQL JOIN INNER JOIN 关键字 LEFT JOIN 关键字 SQL LEFT JOIN 语法 RIGHT JOIN 关键字 SQL RIGHT JOIN 语法 FULL OUTER JOIN 关键字 SQL FULL OUTER JOIN 语法 UNION 操作符 SQL UNION 语法 SQL UNION ALL 语法 SELECT I…

大数据毕业设计选题推荐-B站短视频数据分析系统-Python数据可视化-Hive-Hadoop-Spark

✨作者主页&#xff1a;IT毕设梦工厂✨ 个人简介&#xff1a;曾从事计算机专业培训教学&#xff0c;擅长Java、Python、PHP、.NET、Node.js、GO、微信小程序、安卓Android等项目实战。接项目定制开发、代码讲解、答辩教学、文档编写、降重等。 ☑文末获取源码☑ 精彩专栏推荐⬇…

软媒市场新蓝海:软文媒体自助发布与自助发稿的崛起

在信息时代的浪潮中,软媒市场以其独特的魅力和无限的潜力,成为了企业营销的新宠。随着互联网的飞速发展,软文媒体自助发布平台应运而生,为企业提供了更加高效、便捷的营销方式。而自助发稿功能的加入,更是让软媒市场的蓝海变得更加广阔。 软媒市场的独特价值 软媒市场之所以能…

YOLO_V8分割

YOLO_V8分割 YOLO安装 pip install ultralytics YOLO的数据集转化看csdn 数据标注EIseg EIseg这块&#xff0c;正常安装就好&#xff0c;但是numpy和各类包都容易有冲突&#xff0c;python版本装第一点 数据标注过程中&#xff0c;记得把JSON和COCO都点上&#xff0c;把自…

计算机毕业设计 基于Hadoop平台的岗位推荐系统的设计与实现 Python毕业设计 Python毕业设计选题【附源码+安装调试】

博主介绍&#xff1a;✌从事软件开发10年之余&#xff0c;专注于Java技术领域、Python人工智能及数据挖掘、小程序项目开发和Android项目开发等。CSDN、掘金、华为云、InfoQ、阿里云等平台优质作者✌ &#x1f345;文末获取源码联系&#x1f345; &#x1f447;&#x1f3fb; 精…

python脚本封装APK,轻松随意无须复杂配置

优点&#xff1a;轻松随意不设置&#xff0c;缺点&#xff1a;新用户试用&#xff0c;VI专属能力。 (笔记模板由python脚本于2024年10月11日 06:16:14创建&#xff0c;本篇笔记适合“资深”编程的coder翻阅) 【学习的细节是欢悦的历程】 Python 官网&#xff1a;https://www.py…

基于Springboot+Vue的网上订餐系统(含源码数据库)

1.开发环境 开发系统:Windows10/11 架构模式:MVC/前后端分离 JDK版本: Java JDK1.8 开发工具:IDEA 数据库版本: mysql5.7或8.0 数据库可视化工具: navicat 服务器: SpringBoot自带 apache tomcat 主要技术: Java,Springboot,mybatis,mysql,vue 2.视频演示地址 3.功能 系统中…

@controller注解和@RestController注解详解

文章目录 控制器MVC控制器REST 控制器 controllerRestController总结 控制器 MVC控制器 在了解这两种注解之前我们需要先知道控制器的概念&#xff0c;我们项目中常见的就是MVC控制器&#xff0c;MVC 是一种软件架构模式&#xff0c;它将应用程序分为三个主要组件&#xff1a;…

R语言统计分析——气泡图

参考资料&#xff1a;R语言实战【第2版】 气泡图&#xff08;bubble plot&#xff09;用来展示三个定量变量间的关系&#xff1a;先创建一个二维散点图&#xff0c;然后用点的大小来代表第三个边变量的值。 我们可以使用symbols()函数来创建气泡图。该函数可以在指定的(x,y)坐标…

如何鉴定一段抗体序列的CDR区(CDR1、CDR2、CDR3)

愿武艺晴小朋友一定得每天都开心 如果有一批的抗体序列,可按下面的步骤来处理&#xff1a; (1)可以先处理为fasta的格式&#xff08;或者不处理手动粘贴&#xff09; >20B11 NIVLTQSPASLAVSLGQRATISCKASQSVDYDGDSYMNWYQQKPGQPPKLLIYAASNLESGIPARFSGSGSGTDFTLNIHPVEEEDAA…

华为启动鸿蒙原生应用开发者激励计划 12月31日前上架最高可获10万元

华为开发者官网今天发布了《鸿蒙原生应用开发者激励计划》的公告。此计划是华为给鸿蒙原生应用的开发者提供的现金及流量扶持的专属激励资源&#xff0c;鼓励开发者积极投入鸿蒙原生应用开发&#xff0c;加速应用上架&#xff0c;共创鸿蒙生态。 开发者需要在本计划要求的报名周…

深度学习:手写感知机

文章目录 前言感知机模型训练模型预测小结 前言 上面是一只猫&#xff0c;人类的大脑可以很轻松地识别出。人脑是如何识别的呢&#xff1f; 人类能够识别出这只猫&#xff0c;是因为这张图片具有猫的典型特征。例如&#xff0c;猫的耳朵、眼睛、猫须、嘴巴等独特的形态特征&am…

gcc学习

理论 在使用 GCC (GNU Compiler Collection) 进行 C 或 C 程序的编译时&#xff0c;可以将整个过程分为四个主要阶段&#xff1a;预处理、编译、汇编和链接。下面是每个阶段的命令示例&#xff1a; 1. 预处理-E 预处理阶段会处理所有的预处理指令&#xff08;如 #include 和…

ArcGis JS天地图 暗色地图

方法一&#xff1a;使用css filter 在body下增加svg&#xff0c;并增加需要用到的滤镜&#xff0c;这边用到x-rays <svg id"svgfilters" aria-hidden"true" style"position: absolute; width: 0; height: 0; overflow: hidden"version"…

刷题 排序算法

912. 排序数组 注意这道题目所有 O(n^2) 复杂度的算法都会超过时间限制&#xff0c;只有 O(nlogn) 的可以通过 快速排序空间复杂度为 O(logn)是由于递归的栈的调用归并排序空间复杂度为 O(n) 是由于需要一个临时数组 (当然也需要栈的调用&#xff0c;但是 O(logn) < O(n) 的…

【华为】配置RIP协议

RIP&#xff08;Routing Information Protocol&#xff09;是一种内部网关协议&#xff08;IGP&#xff09;&#xff0c;主要用于小型网络中的动态路由。RIP有两个主要版本&#xff1a;‌RIPv1和‌RIPv2&#xff0c;它们之间存在一些关键区别&#xff1a; ‌分类支持‌&#xf…

利用FnOS搭建虚拟云桌面,并搭建前端开发环境(一)

利用FnOS搭建虚拟云桌面&#xff0c;并搭建前端开发环境 一 飞牛FnOS官方文档一、安装FnOS【Win11系统】1.下载VirtualBox2.下载FnOS镜像3.创建虚拟机4.启动完成后&#xff0c;会进入这样一个界面&#xff0c;这个基本上后续就后台了 本人在网上冲浪了很久&#xff0c;一直也没…

python pyqt5 +vtk 显示obj模型文件

python pyqt5 vtk 显示obj模型文件 准备代码参考 准备 名称版本python3.8.19pyqt55.15.9pyqt5-tools5.15.9.3.3pyqt5-sip12.15.0vtk9.3.1 代码 使用wsl2 和 XLaunch 配合pyqt5进行可视化是可行的。使用pip在conda环境中安装pyqt5相关组件。以下代码在 WSL2 的 Ubuntu 20.04 …