如何修复导致空对象的循环依赖

How to fix this circular dependency causing empty object

我正在使用 node 和 express 构建应用程序的后端。

我将代码的不同部分分隔在不同的文件中:例如,与访问数据库有关的所有内容都在文件 DBService.js 中,如果我想执行与我的用户相关的任何操作,我有一个 UserService.js 文件完成所有应用程序需要的用户,并使用 DBService.js 将用户保存在数据库中。

我知道我的代码中确实存在一些循环依赖关系,但到目前为止一切正常。我几乎所有的事情都使用 GraphQL,但我添加了一个普通的端点来抓取给定 ID 的文件。

我确实需要 index.js(节点应用程序的入口点)中的 FileService.js 来提供文件,这部分工作正常。问题是在我还需要 FileService.js 的另一个文件 (ZoneService.js) 中,它 returns 是一个空对象。

我知道这就是问题所在,因为如果我删除 index.js 文件中的要求,问题就会消失。

这些是导致循环依赖的路径。 '->' 表示上一个服务需要下一个。

FileService -> ZoneService -> FileService

FileService -> ZoneService -> FileUploadService -> FileService

它可能看起来很傻,但我需要它,因为我认为将每个实体的 graphQL 类型定义和解析器保存在它自己的文件中是一个很好的举措。

我将尝试解释我对第一个路径的推理:

我可以将此功能移动到 ZoneService 并从那里获取文件,但这有点破坏我分离关注点的所有逻辑。

我想知道的是解决此问题的最佳方法,以免再次发生,以及如何避免。

我会 post 一些代码,但我不确定是什么,如果您认为有必要,请告诉我。

提前致谢!

编辑 - 这是一些代码:

FileService.js

//Import services to use in resolvers
const EditService = require("./EditService.js") 
const ZoneService = require("./ZoneService.js") 

//Resolvers
const resolvers = {
  Query: {
    getFileById: (parent, {_id}) => {
      return getFileById(_id)
    },
    getFilesById: (parent, {ids}) => {
      return getFilesById(ids)
    },
    getFilesByZoneId: (parent, {_id}) => {
      return getFilesByZoneId(_id)
    },
  },
  File: {
    editHistory: file => {
      return EditService.getEditsById(file.editHistory)
    },
    fileName: file => {
      return file.path.split('\').pop().split('/').pop();
    },
    zone: file => {
      return ZoneService.getZoneById(file.zone)
    }
  }
}

ZoneService.js

//Import services to use in resolvers
const UserService = require("./UserService.js")
const FileService = require("./FileService.js")
const EditService = require("./EditService.js") 
const ErrorService = require("./ErrorService.js") 
const FileUploadService = require("./FileUploadService.js") 

//Resolvers
const resolvers = {
  Query: {
    getZone: (parent, {_id, label}) => {
      return _id ? getZoneById(_id) : getZoneByLabel(label)
    },
    getZones: () => {
      return getZones()
    },
  },
  Zone: {
    author: zone => {
      return UserService.getUserById(zone.author)
    },
    files: zone => {
      if(zone.files && zone.files.length > 0) return FileService.getFilesById(zone.files)
      else return []
    },
    editHistory: zone => {
      return EditService.getEditsById(zone.editHistory)
    }
  },
  Mutation: {
    createZone: async (parent, input, { loggedUser }) => {
      return insertZone(input, loggedUser)
    },
    editZone: async (parent, input, { loggedUser }) => {
      return editZone(input, loggedUser)
    },
    removeZone: async (parent, input, { loggedUser }) => {
      return removeZone(input, loggedUser)
    }
  },
}

为了更好,您应该避免循环依赖。
一个简单的方法是将您的模块分成更小的模块。
作为

FileService -> CommonService
ZoneService -> CommonService

FileServicePartDependsOnZoneService -> ZoneService
ZoneService -> FileServicePartNotDependsOnZoneService

FileService -> ZoneServicePartNotDependsOnFileService 
ZoneServicePartDependsOnFileService -> FileService

请注意,这是示例。您应该将模块命名为有意义且比我的示例更短。

另一种方法是将它们合并在一起。 (但这可能是个坏主意)

如果无法避免循环依赖。您也可以在需要时使用 require 模块而不是 import。 例如:

//FileService.js
let ZoneService

function doSomeThing() {
    if(!ZoneService) {
        ZoneService = require("./ZoneService.js").default
        //or ZoneService = require("./ZoneService.js")
    }
    //using ZoneService
}

为了可重用,定义一个函数getZoneService或其他替代方法

一些应该不应该:

  • Do 将您的模式拆分为更小的模块。对于大多数模式,将类型定义和解析器拆分到多个文件、将相关类型和 Query/Mutation 字段组合在一起是有意义的。解析器和类型定义可能从单个文件导出,或者类型定义可能单独驻留在文件中(可能是扩展名为 .gql.graphql 的纯文本文件)。 (注意:借用 Apollo 的术语,我将把相关的类型定义和解析器称为 模块)。
  • 不要在这些模块之间引入依赖关系。解析器应该彼此独立运行。不需要在另一个内部调用一个解析器——当然也不需要从另一个模块内部调用一个模块的解析器。如果模块之间有一些共享逻辑,将其提取到一个单独的函数中,然后将其导入到两个模块中。
  • 将 API 层与业务逻辑层分开。将业务逻辑包含在您的数据模型 类 中,并让您的解析器远离这些 类。例如,您的应用程序应该有一个 Zone 模型,或者 ZoneServiceZoneRepository 包含 getZoneById 等方法。此文件应包含任何解析器,而应由您的模式模块导入。
  • Do 使用上下文进行依赖注入。您的解析器需要访问的任何数据模型、服务等都应该使用上下文注入。这意味着您无需直接导入这些文件,而是利用上下文参数来访问所需的资源。这使测试更容易,并强制依赖项的单向流。

因此,综上所述,您的项目结构可能如下所示:

services/
  zone-service.js
  file-service.js
schema/
  files/
    typeDefs.gql
    resolvers.js
  zones/
    typeDefs.gql
    resolvers.js

您可以这样初始化您的服务器:

const FileService = require(...)
const ZoneService = require(...)

const server = new ApolloServer({
  typeDefs,
  resolvers,
  context: () => ({
    services: {
      FileService,
      ZoneService,
    }
  })
})

这意味着您的解析器文件不需要导入任何内容,您的解析器看起来就像:

module.exports = {
  Query: {
    getFileById: (parent, {_id}, {services: {FileService}}) => {
      return FileService.getFileById(_id)
    },
    getFilesById: (parent, {ids}, {services: {FileService}}) => {
      return FileService.getFilesById(ids)
    },
    getFilesByZoneId: (parent, {_id}, {services: {FileService}}) => {
      return FileService.getFilesByZoneId(_id)
    },
  },
}