如何解决导致空对象的循环依赖项

时间:2019-06-19 00:21:08

标签: javascript commonjs apollo-server

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

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

我知道我的代码中确实有一些循环依赖项,但到目前为止一切正常。我正在使用GraphQL进行几乎所有操作,但是我添加了一个普通终结点以根据ID来抓取文件。

我确实需要index.js(指向节点应用程序的入口)中的FileService.js来提供文件,并且这一部分工作良好。问题是在另一个我也需要FileService.js的文件(ZoneService.js)中,它返回一个空对象。

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

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

FileService -> ZoneService -> FileService

FileService -> ZoneService -> FileUploadService -> FileService

这看起来很愚蠢,但是我需要这样做,因为我认为将graphQL类型定义和每个实体的解析器保留在自己的文件中是一个好举动。

我将尝试解释我的第一个路径的原因:

  • 我想获取来自某个区域的文件,因此此功能将进入FileService。然后,我使用ZoneService获取给定区域ID的文件ID,然后从数据库获取路径
  • ZoneService需要FileService来解析区域实体中的“文件”字段

我可以将该函数移至ZoneService并从那里获取文件,但是会破坏我分离关注点的所有逻辑。

我想知道的是解决此问题的最佳方法,这样它就不会再次发生,并且该如何避免。

我会发布一些代码,但我不确定该怎么办,如果您认为有必要让我知道。

谢谢!

编辑-这是一些代码:

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)
    }
  },
}

2 个答案:

答案 0 :(得分:3)

几个要做不要

  • 执行将您的架构分为多个较小的模块。对于大多数模式,将类型定义和解析器拆分为多个文件是有意义的,将相关的类型和查询/突变字段分组在一起。解析程序和类型定义可以从单个文件导出,或者类型定义可以单独驻留在文件中(可以是扩展名为.gql.graphql的纯文本文件)。 (注意:借用Apollo的术语,我将把相关的类型定义和解析器称为 module )。
  • 不要介绍这些模块之间的依赖关系。解析器应彼此独立运行。无需在另一个模块内部调用一个解析程序,当然也不需要从另一个模块内部调用一个模块的解析程序。如果模块之间存在某些共享逻辑,请将其提取到一个单独的函数中,然后将其导入两个模块中。
  • 执行,将您的API层与业务逻辑层分开。将业务逻辑包含在数据模型类中,并将解析器排除在这些类之外。例如,您的应用程序应该具有Zone模型,或者包含诸如ZoneService之类的方法的ZoneRepositorygetZoneById。该文件应包含任何解析器,而应由您的模式模块导入。
  • 执行使用上下文进行依赖项注入。解析程序需要访问的任何数据模型,服务等都应使用上下文注入。这意味着您将直接使用context参数访问所需的资源,而不是直接导入这些文件。这使测试更加容易,并强制了单向依赖关系流。

因此,综上所述,您的项目结构可能看起来像这样:

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)
    },
  },
}

答案 1 :(得分:0)

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

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或其他替代方法

相关问题