打字稿FP依赖注入很容易!
#javascript #typescript #node #dependencyinjection

使用框架或库进行依赖注入很容易做到的事情,有些框架支持依赖性注入以进行前端或后端,例如,Nestjs支持依赖性注入后端开发前端开发的模式,并且有许多库可以帮助您进行依赖注入。
但是,对于所有这些软件包和库,他们支持一种使用类的依赖性注入的模式,我认为背后有一个明显的原因,依赖性注入是从Java或C#(例如C#)的OOP编程语言驱动的,并使用A Typescript/Javascript驱动类似的语法,我们作为JS/TS开发人员没有注意到JavaScript不支持一个范式,它支持许多范式,它支持OOP和功能性编程!

当我加入一家公司时,故事开始使用FP,例如使用函数来构建所有后端功能,但是该代码有一个非常明显的问题:“代码不做依赖性注入”
这就是为什么我说它使用fp之类的原因是因为它没有遵循所有功能编程原则,即仅编写功能并直接在功能中使用其依赖项的开发人员。
您可能会问自己,这种模式有什么问题? Lemme列出了我们在此模式上遇到的一些问题

  • 很难测试,因为要测试一个函数,您需要模拟它所需的所有依赖项,并且我们使用了开玩笑,因此我们有很多jest.mock("../filepath"),这使得很难为相同的依赖关系做不同的模拟
  • 代码具有副作用,因为所有功能都是不纯净的函数
  • 很难更改代码库,因为如果该函数取决于另一个函数,并且我们想用另一个函数替换此函数,我们需要更新所有使用它的代码零件,这意味着要重新进行重构以更改一个函数签名,这就是为什么我们需要使用控制原理和依赖注入模式的反转,以便我们能够依赖界面和类型而不是具体实现,因此任何时候我们都希望用另一个功能替换具体实现,我们只能更改该功能!

JavaScript/tyspript中的依赖注入

如果您在某些库中使用类,例如:

,可以在JavaScript/Typescript中进行依赖项注入。
  • justify
  • Nestjs
  • typedi

和其他库,但是在功能编程方面,没有很多库来用于功能,有些人认为依赖项仅适用于OOP,我想说这是错误的,它也适用于功能编程,也适用于功能编程,您可以通过多种方法可以这样做,我建议您阅读有关Medium%20those%20依赖项%20AS%20Arguments的这篇文章。)
它具有非常好的信息,但是现在我将为您提供一些方法,您可以在纯JavaScript中进行依赖注入而无需任何其他依赖项

您可以使用高阶功能的方法之一,但是在给出示例之前,首先定义什么是高阶功能?

在简而言之

在nodejs中,我们所有人都知道回调的概念,基本上接受回调的函数被视为高阶函数

fs.readFile('input.txt', function (err, data) {
   if (err) return console.error(err);
   console.log(data.toString());
});

如您所见,文件系统readFile方法是一个高阶函数,因为它将回调作为参数并执行。

以相同的方式,数组函数诸如mapreduceforEach和其他函数是高阶函数,它们接受要传递的函数作为参数并执行它们。

另一种类型的高阶函数是不接受函数作为参数但它返回函数的函数,您可以像闭合一样考虑它,这意味着在另一个函数中范围范围范围,这是我们需要在功能中进行依赖注入。

这个概念非常容易,在OOP中,我们有这样的课程

class SomeClass {
  constructor() {}

  getUser(userId: string) {
    this.httpService.get('pathHere')
  }
}

我们具有getUser函数,此函数取决于称为httpService的东西,并且将此服务传递给类的方法是将其注入构造函数,因此我们可以做这样的事情:

class SomeClass {
  constructor(httpService: HttpService) {
    this.httpService = httpService;
  }

  getUser(userId: string) {
    this.httpService.get('pathHere')
  }
}

以这种方式,您可以将高阶功能作为OOP中的构造函数,它知道如何返回内部功能并知道其需要的依赖性,因此基本上可以做这样的事情:

function getUser(httpService: HttpService) {
  return async (userId: string) => {
    const result = await httpService.get('pathHere')
  }
}

现在您正在执行更高订单功能,并且还进行依赖注入,因此每当您想消耗getUser功能时,您都可以通过所需的依赖关系,并且它返回可以执行它的函数,因此例如,您会像一样消耗它这:

async function main() {
  const httpService = new HttpService();
  const user = await getUser(httpService)("1234")
}

我们从httpService创建了一个实例,然后将其传递到getUser高阶函数,如您所知,它返回另一个函数,因此我们将用户ID传递给该函数。这很棒,因为现在您可以在一个中测试getUser函数孤立的方式却不了解httpService,因此您可以进行这样的单元测试:

describe('getUser Test Suite', () => {
   const mockedHttpService = {
     get: jest.fn()
   }
   const getUserFunc = getUser(mockedHttpService)
  it("testing getUser", async () => {
     mockedHttpService.get.mockReturnValue({ data: { userId: "1234" } })
     const user = getUserFunc("1234")
  });
});

您可以将任何代替httpService传递给任何内容,并且可以控制它的作用而不了解其具体实现的任何内容,并且您无需使用jest.mock('modulePath')来模拟特定模块!

这足以在实际项目中进行依赖注入吗?

在实际项目中,这种模式很好,但它有一个缺点,这将依赖项从一个函数传递到另一个函数。例如,假设getUser函数是从另一个函数调用的,现在您需要将所需的依赖项注入该功能,并将其注入外部功能,直到您到达项目入口处,您将拥有类似的东西:

基于用户名
从HTTP服务中获取用户的函数

// getUser.ts
function getUser(httpService: HttpService) {
  return async (username: string) => {
    const result = await httpService.get('pathHere')
  }
}

该功能在另一个说明用户是否存在的函数中调用

// checkUserExists.ts
function checkUserExists(httpService: HttpService) {
  return async (username: string) => {
    const user = await getUser(httpService)(username)
    return user ? true : false;
  }
}

并且该函数在创建或不存在之前在创建之前是否存在的createNewuser函数中使用

// createNewUser.ts
function createNewUser(httpService: HttpService) {
  return async ({username}: {username: string}) => {
    const userExists = await checkUserExists(httpService)(username) 
    (username);
    if (userExists) {
      throw new Error("Duplicate user")
    }
   // create the user here
  }
}
function main() {
  const httpService = await new HttpService();
  await createUser(httpService)({ username: 'John' })
}

您可能会观察到您需要将依赖项从外部层次传递到内部层次,这可能会小规模起作用,但是很难大规模管理依赖关系的流动。另外,您还需要知道是否在之前实例化了HttPservice,应将其实例化为单胎或Transit

如何管理依赖关系?

有两种方法:

  • 时间编译
  • 运行时

编译时间依赖注入用于管理依赖项并在程序运行之前通过所有所需的依赖项,这通常是在像Golang这样的编译语言中使用的。

运行时依赖性注入用于在运行时进行依赖项的注入,因此当应用程序实例化时,将开始解决依赖项并在某处缓存并将其注入取决于它的函数/类中。这种模式需要一个重要的东西称为DI container,您可以将其视为将所有创建的依赖项放在其中并从中开始选择以解决使用它的函数/类的地方。

注射X

InjectX是为功能编程中的依赖注入而构建的,它是为此目的而设计的,它使用高阶功能范式在运行时自动注入所需的依赖项。

Injectx具有三个主要组件:

  • di容器:一个池具有所有解决的依赖关系
  • 喷射素:用于告诉Injectx将所需依赖项注入高阶函数
  • 的功能
  • 绑定:用于将依赖关系绑定到容器的函数。

安装Injextx

npm i injectx

您还可以查看here的完整文档

让我们以上面的相同例子为例。您将具有这样的更高订单功能:

// getUser.ts
function GetUser(httpService: HttpService) {
  return async (username: string) => {
    const result = await httpService.get('pathHere')
  }
}

您可以看到,我只是将getUser更改为GetUser,因为每个模块我都会导出两个函数

// getUser.ts
export function GetUser({httpService}: {httpService: HttpService}) {
  return async (username: string) => {
    const result = await httpService.get('pathHere')
  }
}
export const getUser = InjectIn(GetUser)

让我们解释一下我们在这里所做的事情,我在这里导出了两个功能,第一个功能是高阶功能,它期望将某些依赖项注入其中,并且此功能将依赖项作为对象。第二个导出函数是getUser是解决函数。 Injectin基本上采用了高阶功能,并将所需的依赖关系传递给它,并通过解决所有所需的依赖项来返回该高阶功能的内部功能,因此您可以在任何地方使用getUser函数,而无需通过所需的依赖关系

所以你可以拥有这样的东西

import { getUser } from './getUser'
async function main() {
  const user = await getUser("username")
}

您可以看到,无需将httpService依赖关系传递到该函数,因为它是使用InjectIn函数注入的。现在,您需要在调用getUser之前需要的只是将HttpService绑定到容器,并且可以使用此操作:

import { GetContainer } from 'injectx';
const httpService = new HttpService();
GetContainer("default").Bind(httpService);

我们以前谈论过容器,在这里您将依赖项放在其中并开始从中挑选您的需求吗? InjectX允许您创建多个容器,为什么要多个容器,而不仅是一个容器?<​​br> 假设您有以下多个模块:

  • 订单模块
  • 目录模块
  • auth模块

,您想在每个模块的依赖项之间进行分开,以便将依赖关系组织起来,然后您可以创建多个容器,每个容器与一个模块相关,然后将模块依赖项放在相关容器中。


为此,您使用GetContainer函数,它是从indextx导出的函数,它要求indextx获取一个具有特定名称的容器,如果不存在容器,则它将创建一个带有指定名称的新的容器

然后,您可以访问另一种称为Bind的方法,这意味着您要告诉InectX我想将依赖关系附加到该容器中,以便该容器可用于高阶功能。

如果您有兴趣在功能编程中了解更多有关依赖注入的信息,则可以评论bellow并等待下一部分。

感谢您阅读