如何修复导致空对象的循环依赖
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 类型定义和解析器保存在它自己的文件中是一个很好的举措。
我将尝试解释我对第一个路径的推理:
- 我想抓取来自某个区域的文件,所以这个函数进入了 FileService。然后我使用 ZoneService 获取给定区域 ID 的文件 ID,然后我从 DB
获取路径
- ZoneService 需要 FileService 来解析区域实体中的 'files' 字段
我可以将此功能移动到 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
模型,或者 ZoneService
或 ZoneRepository
包含 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)
},
},
}
我正在使用 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 类型定义和解析器保存在它自己的文件中是一个很好的举措。
我将尝试解释我对第一个路径的推理:
- 我想抓取来自某个区域的文件,所以这个函数进入了 FileService。然后我使用 ZoneService 获取给定区域 ID 的文件 ID,然后我从 DB 获取路径
- ZoneService 需要 FileService 来解析区域实体中的 'files' 字段
我可以将此功能移动到 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
模型,或者ZoneService
或ZoneRepository
包含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)
},
},
}