结构体的循环导入

huangapple go评论88阅读模式
英文:

Circular import with structs

问题

我有一个使用Go语言编写的项目,其中包含几个模块。我在处理下面的场景时遇到了循环导入的问题:

详细信息

一个名为Game的模块包含一个包含当前游戏状态的结构体。另一个模块(Modifier)执行一些与游戏相关的操作和计算,因此修改了游戏状态。因此,Modifier需要Game结构体,但不需要Game的任何方法。Modifier从Game中调用,这就导致了循环导入。

问题:#

  • Game初始化Modifier

  • Modifier需要Game结构体

在我看来,这是一个常见的场景,所以我想知道如何以最佳方式解决它。我的解决方案是创建一个名为"Structs"的第三个模块,它只包含整个应用程序的所有结构体。这个解决方案好吗?

英文:

I have a project with several modules in Go. I am having problem with circular imports because of the scenario below:

Details

A module Game contains a struct with the current Game state. Another module (Modifier) is doing some game specific stuff and calculations and therefore modifies the game state. Because of this, Modifier will need the struct Game, but not any methods from Game. Modifier is called from Game and here we have the circular import.

Problem:

  • Game initiates Modifier

  • Modifier needs Game struct

It seems to me that this is a common scenario, so I wonder how I should solve it in the best way. My solution would be to create a third module "Structs" which just contains all the structs for the whole application. Is this a good solution?

答案1

得分: 3

使用第三个包选项:

yourgame/
  state/
    state.go
  modifier/
    modifier.go
  main.go

main.go 将两个组件连接在一起:

import "yourgame/state"
import "yourgame/modifier"

type Game struct {
    state    state.State
    modifier modifier.Modifier
}

func main() {
    // 类似于:
    var game Game
    game.modifier.Modify(game.state)
}

然而,这种方法可能过于紧密耦合。与其操作一个本质上是全局状态对象,不如将数据切分为仅需要修改器所需的部分。

在抽象层面进行推理很困难,所以这里给出一个具体示例来说明我的意思。在你的游戏中:

type Object struct {
    ID, X, Y int
    // 更多数据在这里
}
type Game struct {
    Objects map[int]*Object
}

在你的“modifier”中,假设我们有一个移动对象的 AI 模块。如果他只关心单个对象的位置,你可以创建一个接口:

// 在 yourgame/modifier 中
type Object interface {
    GetCoordinates() (int, int)
    SetCoordinates(int, int)
}
type Modifier struct {}
func (m *Modifier) Update(obj Object) { }

然后,我们只需将这些方法添加到我们原始的 Object 中:

type (obj *Object) GetCoordinates() (int, int) {
    return obj.X, obj.Y
}
type (obj *Object) SetCoordinates(x, y int) {
    obj.X, obj.Y = x, y
}

现在,您可以将对象传递给修改器,而无需循环依赖。

如果你的“modifier”接口最终看起来几乎与你的游戏对象完全相同,那么一个第三个包的结构可能是合理的,这样你就不必总是重复自己。以 net/url 为例考虑。

英文:

With the 3rd package option:

yourgame/
  state/
    state.go
  modifier/
    modifier.go
  main.go

main.go would glue the two components together:

import "yourgame/state"
import "yourgame/modifier"

type Game struct {
    state    state.State
    modifier modifier.Modifier
}

func main() {
    // something like: 
    var game Game
    game.modifier.Modify(game.state)
}

This approach is probably too tightly coupled though. Rather than manipulating an essentially global state object, I would try to slice up the data into just what you need for the modifier.

Reasoning in the abstract is hard, so here's a concrete example of what I mean. In your game:

type Object struct {
    ID, X, Y int
    // more data here
}
type Game struct {
    Objects map[int]*Object
}

In your "modifier", let's suppose we had an AI module that moves an object. If all he cares about is the position of a single object you can create an interface:

// in yourgame/modifier
type Object interface {
    GetCoordinates() (int, int)
    SetCoordinates(int, int)
}
type Modifier struct {}
func (m *Modifier) Update(obj Object) { }

Then we just have to add those methods to our original Object:

type (obj *Object) GetCoordinates() (int, int) {
    return obj.X, obj.Y
}
type (obj *Object) SetCoordinates(x, y int) {
    obj.X, obj.Y = x, y
}

And now you can pass objects to your modifier without needing a cyclic dependency.

Now if it turns out your "modifier" interface ends up looking almost exactly the same as your game object, then a 3rd package of structs is probably reasonable so you aren't always repeating yourself. For an example consider net/url.

答案2

得分: 0

通常情况下,如果包B中的代码直接读取/修改A.Type,那么该代码应该在包A中。至少其中需要直接访问的部分应该在包A中。

要在分开的包AB之间拆分某些内容,你通常会尝试隔离一个用于访问A.Type的API,该API可以表示为一个接口。然后,B将定义并使用此接口,而A.Type将实现它(隐式地,无需包含B对其的定义)。

然后,某个东西(可能是A,也可能是一个单独的包)将通过传递适当的A.Type*A.Type值来使用B

或者,根据你的设计,关系可以反过来,B.OtherType隐式地实现了A定义并使用的接口。
或者,AB都可以只通过接口相互使用;这完全取决于具体情况。

例如,可能是这样的:

package Game // "A"

type State struct {
        data int // etc
}

func (s State) IsValid() bool          { return true }
func (s *State) ChangeY(arg int) error { return nil }
// …etc…

和:

package Modifier // "B"

type GameState interface {
        IsValid() bool
        ChangeY(int) error
}

type M struct {
        s GameState
        //…
}

func New(s GameState) *M {
        return &M{s: s}
}

func (m M) DoSomething() {
        if s.IsValid() {
                // …
        }
        s.ChangeY(42)
        // …etc…
}
英文:

Normally if package B has code that directly reads/modifies A.Type then that code should be in package A.
At least the parts of it if that need direct access should.

To split something between separate packages A and B you'd normal try to isolate an API for access to A.Type that can be expressed as an interface. Then B would define and use this interface and A.Type would implement it (implicitly, without needing to include B's definition of it).

Then something (possibly A, possibily a separate package) would use B by passing an A.Type or *A.Type value as appropriate.

Or depending on your design the relationship could be reversed, with B.OtherType implicitly implementing an interface defined and used by A.
Or both A and B could use each other only through interfaces; it all depends on the details.

E.g. perhaps something like:

package Game // "A"

type State struct {
        data int // etc
}

func (s State) IsValid() bool          { return true }
func (s *State) ChangeY(arg int) error { return nil }
// …etc…

and:

package Modifier // "B"

type GameState interface {
        IsValid() bool
        ChangeY(int) error
}

type M struct {
        s GameState
        //…
}

func New(s GameState) *M {
        return &M{s: s}
}

func (m M) DoSomething() {
        if s.IsValid() {
                // …
        }
        s.ChangeY(42)
        // …etc…
}

答案3

得分: 0

我会为你翻译以下内容:

我会在同一个包中定义类型(在这种情况下是Game)以及它的所有方法。根据语言规范,你甚至不能为从另一个包导入的类型定义方法。

//你应该先这样做
type MyPackageType ImportedType
//然后才能这样做
func (foo MyPackageType) Modify() {
...
}
英文:

I'd define type(Game in this case) and all its methods in same one package. You can't even define methods on type imported from another package according to language spec,

//you should first do 
type MyPackageType ImportedType
//and only then
func (foo MyPackageType) Modify() {
...
}

huangapple
  • 本文由 发表于 2015年5月4日 19:54:28
  • 转载请务必保留本文链接:https://go.coder-hub.com/30029502.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定