'Fixing import cycle in Go
So I have this import cycle which I'm trying to solve. I have this following pattern:
view/
- view.go
action/
- action.go
- register.go
And the general idea is that actions are performed on a view, and are executed by the view:
// view.go
type View struct {
Name string
}
// action.go
func ChangeName(v *view.View) {
v.Name = "new name"
}
// register.go
const Register = map[string]func(v *view.View) {
"ChangeName": ChangeName,
}
And then in view.go we invoke this:
func (v *View) doThings() {
if action, exists := action.Register["ChangeName"]; exists {
action(v)
}
}
But this causes a cycle because View depends on the Action package, and vice versa. How can I solve this cycle? Is there a different way to approach this?
Solution 1:[1]
An import cycle indicates a fundamentally faulty design. Broadly speaking, you're looking at one of the following:
- You're mixing concerns. Perhaps
view
shouldn't be accessingaction.Register
at all, or perhapsaction
shouldn't be responsible for changing the names of views (or both). This seems the most likely. - You're relying on a concretion where you should be relying on an interface and injecting a concretion. For example, rather than the view accessing
action.Register
directly, it could call a method on an interface type defined withinview
, and injected into theView
object when it is constructed. - You need one or more additional, separate packages to hold the logic used by both the
view
andaction
packages, but which calls out to neither.
Generally speaking, you want to architect an application so that you have three basic types of packages:
- Wholly self-contained packages, which reference no other first-party packages (they can of course reference the standard library or other third-party packages).
- Logic packages whose only internal dependencies are of type 1 above, i.e., wholly self-contained packages. These packages should not rely on each other or on those of type 3 below.
- "Wiring" packages, which mostly interact with the logic packages, and handle instantiation, initialization, configuration, and injection of dependencies. These can depend on any other package except for other type 3 packages. You should need very, very few packages of this type - often just one,
main
, but occasionally two or three for more complex applications.
Solution 2:[2]
Import cycles are the result of a design error. Structs which depend on each other in both directions must be in the same package, or else an import cycle will occur. By the way, Go is not the only programming language with this restriction. It also exist in C++ and Python, for example.
Solution 3:[3]
Basically, you are able to break dependencies by introducing interface and injecting the interface instead of a struct.
With your example it would look like:
// view.go
package view
import "import_cycles/action"
type View struct {
Name string
}
func (v *View) ModifyName(name string) {
v.Name = name
}
func (v *View) DoThings() {
if action, exists := action.Register["ChangeName"]; exists {
action(v)
}
}
// action.go
package action
func ChangeName(v NameChanger) {
v.ModifyName("new name")
}
// register.go
package action
type NameChanger interface {
ModifyName(name string)
}
var Register = map[string]func(v NameChanger){
"ChangeName": ChangeName,
}
Please note that NameChanger
interface is introduced. Here following things to be point:
- this interface injected in function ChangeName instead of passing struct
- struct View is implementing this interface
As a result package "action" no more need to import package "view" since the interface is placed in the same package "action"
In main.go we can test the result:
v := &view.View{
Name: "some name",
}
v.DoThings()
fmt.Println(v)
// &{new name}
Sources
This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.
Source: Stack Overflow
Solution | Source |
---|---|
Solution 1 | Adrian |
Solution 2 | |
Solution 3 | sprutex |