简体   繁体   中英

How to manage cyclic dependencies when have interface and its implementation in different packages

I have my project structure looks like this:

Structure of code:

hypervisor
├── hypervisor.go
├── hyperv
│   └── hyperv.go
└── virtualbox
    ├── vbox.go
    └── vboxprops.go

Source code:

//hypervisor/hypervisor.go
package hypervisor

type Hypervisor interface {
    Start(vmName string) error

    ListMounts(vmName string) ([]MountPath, error)

    //....
}


type MountPath struct {
    HostPath  string
    GuestPath string
}


func detect() (Hypervisor, error) {
    return &virtualbox.Virtualbox{}, nil  // <<1 HERE
}

// ... other code

And have another (nested) package :

//hypervisor/virtualbox/vbox.go
package virtualbox

type Virtualbox struct {
}

func (*Virtualbox) Start(vmName string) error {
    return vboxManage("startvm", vmName, "--type", "headless").Run()
}

func (*Virtualbox) ListMounts(vmName string) ([]hypervisor.MountPath, error) { // <<2 HERE
    // ....
} 

// ... other code

And as seen, of course, such code leads to import cycle not allowed . because of:

  1. hypervisor pcakge referencing virtualbox.VirtualBox type
  2. virtualbox package referencing hypervisor.MountPath type

I know if I move the struct MounthPath to another package would solve the issue, but I don't think is the correct solution design-wise.

Any suggestion?

One of easiest way I would do is to separate entities into entities package for example (in this case: the Hypervisor and Virtualbox struct are entities or whatever you want to call it).
This is most common design I think, so every struct that inner packages use will not cause cyclic deps.
Example of usage: all time package structs are on top package level. time.Time{} , time.Duration{} , etc. time.Duration does not sit on time/duration package.

Following suggestions from Dave Cheney to define interfaces by the caller will avoid cycle dependencies in most cases. But this will only solve flat data models. In your case, you have nested entities ie., HyperVisor has fucntion which returns MounthPath. We can model this in two ways

  1. Define MouthPath in separate package (like you suggested). In addition, defining the interface in the virtualbox package will help in long term to provide alternative implementation for Hypervisor.

  2. Let virtualbox define both Hypervisor and MounthPath as interface. One disadvantage is that the hypervisor implementing package use virtualbox.MouthPath interface to satisfy the interface when passed like below.

//hypervisor/hypervisor.go

package hypervisor

type Hypervisor struct{
     someField []virtualbox.MountPath
}

type MountPath struct { // this can be used as virtualbox.MountPath
    hostPath  string
    guestPath string
}

func (m *MountPath) HostPath() string { return m.hostPath }
func (m *MountPath) GuestPath() string { return m.guestPath }

func detect() (Hypervisor, error) {
    return &virtualbox.Virtualbox{}, nil  // <<1 HERE
}

And have another package (Need not be nested)

//hypervisor/virtualbox/vbox.go
package virtualbox

type Hypervisor interface {
    Start(vmName string) error

    ListMounts(vmName string) ([]MountPath, error)

    //....
} 

type MountPath interface {
        HostPath()  string
        GuestPath() string
}

type Virtualbox struct {}

func (*Virtualbox) Start(vmName string) error {
    return vboxManage("startvm", vmName, "--type", "headless").Run()
}

func (*Virtualbox) ListMounts(vmName string) ([]MountPath, error) { // <<2 HERE
    // ....
} 

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM