【问题标题】:How to fix this circular dependency causing empty object如何修复导致空对象的循环依赖
【发布时间】:2019-11-01 15:45:51
【问题描述】:

我正在使用 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 并从那里获取文件,但这会有点破坏我分离关注点的所有逻辑。

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

我会发布一些代码,但我不确定如果您认为有必要,请告诉我。

提前致谢!

编辑 - 这是一些代码:

文件服务.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 是一个对象/类吗?
  • @DanielRearden 我为 FileService 和 ZoneService 添加了代码示例,如果您需要更多,请告诉我。
  • @RandyCasburn FileService 是一个模块(所以它就像一个对象,对吧?),它包含函数和带有解析器和类型定义等内容的 graphQL 对象。当我需要文件时,我正在使用 module.exports 使它们可用。

标签: javascript commonjs apollo-server


【解决方案1】:

注意事项注意事项

  • 不要将您的架构拆分为更小的模块。对于大多数模式,将类型定义和解析器拆分到多个文件中,将相关类型和 Query/Mutation 字段组合在一起是有意义的。解析器和类型定义可能从单个文件中导出,或者类型定义可能单独存在于一个文件中(可能是带有.gql.graphql 扩展名的纯文本文件)。 (注:借用 Apollo 的术语,我将把相关的类型定义和解析器称为一个模块)。
  • 不要在这些模块之间引入依赖关系。解析器应该彼此独立运行。没有必要在另一个内部调用一个解析器——当然也不需要从另一个模块内部调用一个模块的解析器。如果模块之间有一些共享逻辑,请将其提取到单独的函数中,然后将其导入到两个模块中。
  • 将您的 API 层与业务逻辑层分开。将业务逻辑包含在您的数据模型类中,并使您的解析器远离这些类。例如,您的应用程序应该有一个 Zone 模型,或者 ZoneServiceZoneRepository 包含像 getZoneById 这样的方法。此文件应该包含任何解析器,而应该由您的架构模块导入。
  • 不要使用上下文进行依赖注入。解析器需要访问的任何数据模型、服务等都应该使用上下文注入。这意味着您将使用 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)
    },
  },
}

【讨论】:

  • 这是我第一次使用 GraphQL。我知道 context 选项并且我正在使用它来访问登录的用户,但我没有想到使用它来注入服务等依赖项,这是一个很棒的功能!谢谢你的回答。
  • 我不应该将模式内容混入业务逻辑中。既然你指出来了,那就没有意义了。我想我花了太多时间来寻找如何将架构分成多个文件,我只是没有过多地考虑它。
  • 只有一个关于您的代码的问题:context() => ({...})。我不明白围绕我认为应该是功能代码块的内容。和使用return一样吗?我在这里错过了什么?
  • @BrunoTavares 如果在箭头函数中省略大括号,它将返回箭头后面的任何内容。如果大括号存在,则其中的内容被视为语句。对象也使用大括号。如果我们想使用这种速记语法返回一个对象,我们必须用括号括起来,这样大括号内的内容将被视为一个对象,而不是一组语句。见here
  • 感谢您的解释和提供的链接!我开始使用这种新语法,但我需要好好阅读它以了解所有技巧。
【解决方案2】:

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

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

【讨论】:

  • 我同意你的方法,我试图想办法将代码分成更小的模块,但以我目前的逻辑我找不到实现它的方法。我添加了一些代码示例,请随意查看。
  • @BrunoTavares 看起来你只是定义了resolvers,然后GraphQL 定义了其余的东西,比如getFilesByIdgetZoneById。我对吗?我还没有使用 GraphQL 的经验。
  • 不,这些是我为查询数据库而编写的函数。解析器是 GraphQL 的一部分。 GraphQL 解析器的目的是返回不是原始类型的特定字段的值。例如 Zone 有一个 id 属性,它是一个字符串,这是一个原始类型,所以没问题。但是它有一个作者属性是用户(由我定义)类型,但 GraphQL 不知道,所以需要一个自定义解析器来获取正确的值。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2020-02-28
  • 1970-01-01
  • 2021-09-27
  • 2015-06-08
  • 2018-07-22
相关资源
最近更新 更多